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

16 KiB
Raw Blame History

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%)

  • 添加 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)