# 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 = {} 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 = {} 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 = {} 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) 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) => (
{photo.adminVisibleOverride === false ? ( // 隐藏状态 UI
{/* ... */}
) : ( // 显示状态 UI <> {/* ... 大量 JSX */} )}
))} ``` **问题**: - 条件渲染导致两个分支都被评估 - 可以提取为单独的组件 **优化建议**: ```typescript // 提取为单独组件 const PhotoCard = ({ photo, isOwner, currentTree, onSelect, onToggle, isLoading }) => { if (photo.adminVisibleOverride === false) { return } return } ``` --- ## 📋 优化建议总结 | 优先级 | 问题 | 预期性能提升 | 实现难度 | |--------|------|------------|---------| | 🔴 高 | 照片分组逻辑重复计算 | 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)