590 lines
16 KiB
Markdown
590 lines
16 KiB
Markdown
# 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)
|
||
|