16 KiB
16 KiB
app/page.tsx 性能分析报告
📊 执行摘要
该文件是一个大型 React 组件(788 行),存在多个性能问题。主要问题包括:
- 重复计算问题:多处在 render 中重复计算相同数据
- 缺失 useMemo 优化:复杂计算未被 memoized
- 缺失 useCallback 优化:事件处理器未被 memoized
- 组件过大:单个组件承载过多功能
- 列表渲染问题:照片分组逻辑在每次 render 时重新计算
- 不必要的条件渲染:导致额外的计算开销
🔴 严重问题
1. 照片分组逻辑重复计算(第 ~750-800 行)
问题描述:
{(() => {
// 按月份分组
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+ 张照片,性能下降明显
- 创建大量临时对象
优化建议:
// 使用 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 行)
问题描述:
{(() => {
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 都重新计算所有成员的生日和忌日
- 农历转换函数调用多次
- 日期对象创建过多
- 代码重复度高
优化建议:
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 行)
问题描述:
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],这会导致整个对象变化时重新计算 - 应该更精细地指定依赖项
优化建议:
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 行):
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 失效,每次都创建新函数
优化建议:
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 行):
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是整个对象 - 应该更精细地指定依赖项
优化建议:
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 行):
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简化代码
优化建议:
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 行):
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 个
优化建议:
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 行):
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 等)
- 可以合并为单次遍历
优化建议:
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 行):
{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>
))}
问题:
- 条件渲染导致两个分支都被评估
- 可以提取为单独的组件
优化建议:
// 提取为单独组件
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%)
- 添加
groupedPhotosByMonthuseMemo - 添加
upcomingEventsuseMemo - 修复所有 useMemo 依赖项
第二阶段(优化,预期提升 10-15%)
- 优化
stats计算为单次遍历 - 修复
handleAdminPhotoToggleuseCallback - 提取照片卡片为单独组件
第三阶段(重构,预期提升 20-30%)
- 拆分大型组件为子组件
- 实现虚拟滚动(如果照片数量很多)
- 添加性能监控
📊 性能指标建议
使用 React DevTools Profiler 测量:
- 组件渲染时间
- 不必要的重新渲染
- 依赖项变化频率
使用 Web Vitals 测量:
- LCP (Largest Contentful Paint)
- FID (First Input Delay)
- CLS (Cumulative Layout Shift)