Files
chinese-family-tree-2/PERFORMANCE_ANALYSIS.md
T
freedakgmail e9822f5c92 0.9.0.0
2025-12-22 07:52:55 +08:00

590 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app/page.tsx 性能分析报告
## 📊 执行摘要
该文件是一个大型 React 组件(788 行),存在多个性能问题。主要问题包括:
- **重复计算问题**:多处在 render 中重复计算相同数据
- **缺失 useMemo 优化**:复杂计算未被 memoized
- **缺失 useCallback 优化**:事件处理器未被 memoized
- **组件过大**:单个组件承载过多功能
- **列表渲染问题**:照片分组逻辑在每次 render 时重新计算
- **不必要的条件渲染**:导致额外的计算开销
---
## 🔴 严重问题
### 1. **照片分组逻辑重复计算(第 ~750-800 行)**
**问题描述**
```typescript
{(() => {
// 按月份分组
const sortedPhotos = [...allPhotos].sort((a, b) =>
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
)
const groupedByMonth: Record<string, typeof allPhotos> = {}
sortedPhotos.forEach(photo => {
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
if (!groupedByMonth[monthKey]) {
groupedByMonth[monthKey] = []
}
groupedByMonth[monthKey].push(photo)
})
return Object.entries(groupedByMonth).map(...)
})()}
```
**影响**
- 每次 render 都重新排序和分组所有照片
- 如果有 100+ 张照片,性能下降明显
- 创建大量临时对象
**优化建议**
```typescript
// 使用 useMemo 缓存分组结果
const groupedPhotosByMonth = useMemo(() => {
const sortedPhotos = [...allPhotos].sort((a, b) =>
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
)
const groupedByMonth: Record<string, typeof allPhotos> = {}
sortedPhotos.forEach(photo => {
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
if (!groupedByMonth[monthKey]) {
groupedByMonth[monthKey] = []
}
groupedByMonth[monthKey].push(photo)
})
return Object.entries(groupedByMonth)
}, [allPhotos])
```
---
### 2. **未来三月纪念日计算重复(第 ~600-900 行)**
**问题描述**
```typescript
{(() => {
const now = new Date()
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
const members = Object.values(treeData.members)
const upcomingEvents: Array<{...}> = []
members.forEach(member => {
// 生日计算
if (member.birthDate) {
const birthDate = new Date(member.birthDate)
let thisYearBirth: Date
let lunarDisplay: string | undefined
if (member.isLunarDate) {
const lunarInfo = solar2lunar(birthDate)
if (lunarInfo) {
const thisYearLunar = lunar2solar(...)
// ... 复杂逻辑
}
}
// ... 更多逻辑
}
// 忌日计算(几乎相同的逻辑重复)
if (member.deathDate) {
// ... 重复的逻辑
}
})
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
// ... 返回 JSX
})()}
```
**影响**
- 每次 render 都重新计算所有成员的生日和忌日
- 农历转换函数调用多次
- 日期对象创建过多
- 代码重复度高
**优化建议**
```typescript
const upcomingEvents = useMemo(() => {
const now = new Date()
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
const members = Object.values(treeData.members)
const events: Array<{...}> = []
members.forEach(member => {
// 提取为单独函数
const birthEvents = calculateBirthdayEvents(member, now, threeMonthsLater)
const deathEvents = calculateDeathEvents(member, now, threeMonthsLater)
events.push(...birthEvents, ...deathEvents)
})
return events.sort((a, b) => a.date.getTime() - b.date.getTime())
}, [treeData.members])
```
---
### 3. **本月纪念日计算重复(第 ~400-500 行)**
**问题描述**
```typescript
const monthlyAnniversaries = useMemo(() => {
const members = Object.values(treeData.members)
const anniversaries: Array<{...}> = []
members.forEach(member => {
// 生日检查
if (member.birthDate) {
const isLunar = member.isLunarDate || false
const result = isInCurrentMonth(member.birthDate, isLunar)
if (result) {
let lunarDisplay = undefined
if (isLunar) {
const lunar = solar2lunar(new Date(member.birthDate))
if (lunar) {
lunarDisplay = `${lunar.monthName}${lunar.dayName}`
}
}
anniversaries.push({...})
}
}
// 忌日检查(重复逻辑)
if (member.deathDate) {
// ... 几乎相同的代码
}
})
const sorted = anniversaries.sort((a, b) => a.day - b.day)
return sorted
}, [treeData])
```
**问题**
- 虽然使用了 useMemo,但依赖项是 `[treeData]`,这会导致整个对象变化时重新计算
- 应该更精细地指定依赖项
**优化建议**
```typescript
const monthlyAnniversaries = useMemo(() => {
const members = Object.values(treeData.members)
const anniversaries: Array<{...}> = []
members.forEach(member => {
// 提取为单独函数
const birthAnniversary = createAnniversary(member, 'birth')
const deathAnniversary = createAnniversary(member, 'death')
if (birthAnniversary) anniversaries.push(birthAnniversary)
if (deathAnniversary) anniversaries.push(deathAnniversary)
})
return anniversaries.sort((a, b) => a.day - b.day)
}, [treeData.members]) // 更精细的依赖项
```
---
## 🟡 中等问题
### 4. **handleAdminPhotoToggle 缺失 useCallback**
**问题描述**(第 ~350 行):
```typescript
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
if (!isOwner) return
const member = treeData.members[memberId]
if (!member || !member.photos || member.photos.length === 0) return
const key = `${memberId}|${photoUrl}`
setAdminToggleLoading(key)
try {
const updatedPhotos = (member.photos as FamilyPhoto[]).map(photo =>
photo.url === photoUrl ? { ...photo, adminVisibleOverride: value } : photo
)
await updateMember(memberId, { photos: updatedPhotos })
} catch (error) {
console.error('更新管理员展示权限失败:', error)
} finally {
setAdminToggleLoading(null)
}
}, [isOwner, treeData.members, updateMember])
```
**问题**
- 依赖项包含 `treeData.members`,这是一个对象,每次都会创建新引用
- 导致 useCallback 失效,每次都创建新函数
**优化建议**
```typescript
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
if (!isOwner) return
setAdminToggleLoading(`${memberId}|${photoUrl}`)
try {
const member = treeData.members[memberId]
if (!member?.photos?.length) return
const updatedPhotos = member.photos.map(photo =>
photo.url === photoUrl ? { ...photo, adminVisibleOverride: value } : photo
)
await updateMember(memberId, { photos: updatedPhotos })
} catch (error) {
console.error('更新管理员展示权限失败:', error)
} finally {
setAdminToggleLoading(null)
}
}, [isOwner, updateMember]) // 移除 treeData.members 依赖
```
---
### 5. **allPhotos 计算中的重复过滤**
**问题描述**(第 ~300-330 行):
```typescript
const allPhotos = useMemo(() => {
const photos: Array<{...}> = []
Object.values(treeData.members).forEach(member => {
if (member.photos && member.photos.length > 0) {
(member.photos as FamilyPhoto[]).forEach(photo => {
const visible = photo.visibleInOverview ?? false
if (!visible) return // ❌ 第一次过滤
const adminAllowed = photo.adminVisibleOverride ?? true
if (!adminAllowed && !isOwner) return // ❌ 第二次过滤
photos.push({...})
})
}
})
return photos.sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime())
}, [treeData, isOwner])
```
**问题**
- 依赖项是 `[treeData, isOwner]`,但 `treeData` 是整个对象
- 应该更精细地指定依赖项
**优化建议**
```typescript
const allPhotos = useMemo(() => {
const photos: Array<{...}> = []
Object.values(treeData.members).forEach(member => {
if (!member.photos?.length) return
member.photos.forEach(photo => {
// 合并过滤条件
if (!photo.visibleInOverview) return
if (!photo.adminVisibleOverride && !isOwner) return
photos.push({
url: photo.url,
caption: photo.caption,
uploadedAt: photo.uploadedAt,
memberId: member.id,
memberName: member.fullName,
isDead: !!member.deathDate,
adminVisibleOverride: photo.adminVisibleOverride ?? true,
visibleInOverview: photo.visibleInOverview,
})
})
})
return photos.sort((a, b) =>
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
)
}, [treeData.members, isOwner]) // 更精细的依赖项
```
---
### 6. **locationGroups 计算可优化**
**问题描述**(第 ~550-570 行):
```typescript
const locationGroups = useMemo(() => {
const members = Object.values(treeData.members)
const groups: Record<string, any[]> = {}
members.forEach(member => {
if (member.ancestralHome) {
if (!groups[member.ancestralHome]) {
groups[member.ancestralHome] = []
}
groups[member.ancestralHome].push(member)
}
})
return Object.entries(groups).sort((a, b) => b[1].length - a[1].length)
}, [treeData])
```
**问题**
- 依赖项是 `[treeData]`,应该是 `[treeData.members]`
- 可以使用 `reduce` 简化代码
**优化建议**
```typescript
const locationGroups = useMemo(() => {
const members = Object.values(treeData.members)
const groups = members.reduce((acc, member) => {
if (member.ancestralHome) {
if (!acc[member.ancestralHome]) {
acc[member.ancestralHome] = []
}
acc[member.ancestralHome].push(member)
}
return acc
}, {} as Record<string, typeof members>)
return Object.entries(groups).sort((a, b) => b[1].length - a[1].length)
}, [treeData.members])
```
---
### 7. **recentMembers 计算可优化**
**问题描述**(第 ~280-290 行):
```typescript
const recentMembers = useMemo(() => {
return Object.values(treeData.members)
.sort((a, b) => parseInt(b.id) - parseInt(a.id))
.slice(0, 5)
}, [treeData])
```
**问题**
- 依赖项是 `[treeData]`,应该是 `[treeData.members]`
- 每次都排序整个数组,即使只需要前 5 个
**优化建议**
```typescript
const recentMembers = useMemo(() => {
const members = Object.values(treeData.members)
// 使用堆排序或部分排序会更高效
// 但对于小数据集,简单排序也可以
return members
.sort((a, b) => parseInt(b.id) - parseInt(a.id))
.slice(0, 5)
}, [treeData.members])
```
---
## 🟢 轻微问题
### 8. **stats 计算中的重复数组操作**
**问题描述**(第 ~200-250 行):
```typescript
const stats = useMemo(() => {
const members = Object.values(treeData.members)
const totalMembers = members.length
// 多次遍历数组
const livingMembers = members.filter(m => !m.deathDate).length
const deceasedMembers = members.filter(m => m.deathDate).length
const maleCount = members.filter(m => m.gender === 'MALE').length
const femaleCount = members.filter(m => m.gender === 'FEMALE').length
const generations = members.map(m => m.generation || 0)
const maxGeneration = generations.length > 0 ? Math.max(...generations) : 0
// ... 更多计算
}, [treeData])
```
**问题**
- 多次遍历 members 数组(filter, map 等)
- 可以合并为单次遍历
**优化建议**
```typescript
const stats = useMemo(() => {
const members = Object.values(treeData.members)
const totalMembers = members.length
// 单次遍历计算所有统计数据
let livingMembers = 0
let deceasedMembers = 0
let maleCount = 0
let femaleCount = 0
let maxGeneration = 0
const birthYears: number[] = []
let totalAge = 0
let deceasedWithAgeCount = 0
members.forEach(m => {
if (m.deathDate) {
deceasedMembers++
} else {
livingMembers++
}
if (m.gender === 'MALE') maleCount++
else if (m.gender === 'FEMALE') femaleCount++
if (m.generation && m.generation > maxGeneration) {
maxGeneration = m.generation
}
if (m.birthDate) {
const birthYear = new Date(m.birthDate).getFullYear()
birthYears.push(birthYear)
if (m.deathDate) {
const deathYear = new Date(m.deathDate).getFullYear()
totalAge += deathYear - birthYear
deceasedWithAgeCount++
}
}
})
const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear()
const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0
const averageLifespan = deceasedWithAgeCount > 0 ? Math.round(totalAge / deceasedWithAgeCount) : 0
return {
totalMembers,
livingMembers,
deceasedMembers,
maleCount,
femaleCount,
maxGeneration,
yearsSpan,
earliestYear,
averageLifespan
}
}, [treeData.members])
```
---
### 9. **组件过大,需要拆分**
**问题描述**
- 单个组件有 788 行代码
- 包含多个独立的功能模块:
- 统计卡片
- 照片展示
- 纪念日管理
- 活动日志
- 籍贯记录
**优化建议**
拆分为以下子组件:
```
DashboardPage (主组件)
├── StatsSection (统计概览)
├── PhotosTab
│ └── PhotoGallery (照片库)
├── RecentTab
│ ├── AnniversariesSection (纪念日)
│ └── ActivityLogSection (活动日志)
├── StatisticsTab
│ └── StatisticsCharts (已动态导入)
└── MigrationTab
└── LocationGroups (籍贯记录)
```
---
### 10. **条件渲染中的重复计算**
**问题描述**(第 ~750-800 行):
```typescript
{monthPhotos.map((photo, index) => (
<div key={`${photo.memberId}-${index}`} className="break-inside-avoid group">
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
{photo.adminVisibleOverride === false ? (
// 隐藏状态 UI
<div className="p-4 space-y-2">
{/* ... */}
</div>
) : (
// 显示状态 UI
<>
{/* ... 大量 JSX */}
</>
)}
</div>
</div>
))}
```
**问题**
- 条件渲染导致两个分支都被评估
- 可以提取为单独的组件
**优化建议**
```typescript
// 提取为单独组件
const PhotoCard = ({ photo, isOwner, currentTree, onSelect, onToggle, isLoading }) => {
if (photo.adminVisibleOverride === false) {
return <HiddenPhotoCard photo={photo} isOwner={isOwner} onToggle={onToggle} isLoading={isLoading} />
}
return <VisiblePhotoCard photo={photo} isOwner={isOwner} currentTree={currentTree} onSelect={onSelect} onToggle={onToggle} isLoading={isLoading} />
}
```
---
## 📋 优化建议总结
| 优先级 | 问题 | 预期性能提升 | 实现难度 |
|--------|------|------------|---------|
| 🔴 高 | 照片分组逻辑重复计算 | 20-30% | 低 |
| 🔴 高 | 未来三月纪念日重复计算 | 15-25% | 中 |
| 🔴 高 | 本月纪念日依赖项优化 | 10-15% | 低 |
| 🟡 中 | handleAdminPhotoToggle useCallback 优化 | 5-10% | 低 |
| 🟡 中 | allPhotos 依赖项优化 | 5-10% | 低 |
| 🟡 中 | locationGroups 依赖项优化 | 3-5% | 低 |
| 🟡 中 | stats 单次遍历优化 | 10-15% | 中 |
| 🟢 低 | 组件拆分 | 20-30% | 高 |
| 🟢 低 | 条件渲染优化 | 5-10% | 中 |
---
## 🚀 快速修复清单
### 第一阶段(立即修复,预期提升 30-40%)
- [ ] 添加 `groupedPhotosByMonth` useMemo
- [ ] 添加 `upcomingEvents` useMemo
- [ ] 修复所有 useMemo 依赖项
### 第二阶段(优化,预期提升 10-15%)
- [ ] 优化 `stats` 计算为单次遍历
- [ ] 修复 `handleAdminPhotoToggle` useCallback
- [ ] 提取照片卡片为单独组件
### 第三阶段(重构,预期提升 20-30%)
- [ ] 拆分大型组件为子组件
- [ ] 实现虚拟滚动(如果照片数量很多)
- [ ] 添加性能监控
---
## 📊 性能指标建议
使用 React DevTools Profiler 测量:
- 组件渲染时间
- 不必要的重新渲染
- 依赖项变化频率
使用 Web Vitals 测量:
- LCP (Largest Contentful Paint)
- FID (First Input Delay)
- CLS (Cumulative Layout Shift)