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

10 KiB

app/page.tsx 优化实现指南

📋 实现步骤

第一阶段:快速修复(预期时间:1-2 小时)

步骤 1: 修复 useMemo 依赖项

文件: app/page.tsx

修改位置 1: 第 ~280-290 行 - recentMembers

// 修改前
const recentMembers = useMemo(() => {
  return Object.values(treeData.members)
    .sort((a, b) => parseInt(b.id) - parseInt(a.id))
    .slice(0, 5)
}, [treeData])

// 修改后
const recentMembers = useMemo(() => {
  return Object.values(treeData.members)
    .sort((a, b) => parseInt(b.id) - parseInt(a.id))
    .slice(0, 5)
}, [treeData.members])

修改位置 2: 第 ~300-330 行 - allPhotos

// 修改前
const allPhotos = useMemo(() => {
  // ... 代码
}, [treeData, isOwner])

// 修改后
const allPhotos = useMemo(() => {
  // ... 代码
}, [treeData.members, isOwner])

修改位置 3: 第 ~400-500 行 - monthlyAnniversaries

// 修改前
const monthlyAnniversaries = useMemo(() => {
  // ... 代码
}, [treeData])

// 修改后
const monthlyAnniversaries = useMemo(() => {
  // ... 代码
}, [treeData.members])

修改位置 4: 第 ~550-570 行 - locationGroups

// 修改前
const locationGroups = useMemo(() => {
  // ... 代码
}, [treeData])

// 修改后
const locationGroups = useMemo(() => {
  // ... 代码
}, [treeData.members])

修改位置 5: 第 ~200-250 行 - stats

// 修改前
const stats = useMemo(() => {
  // ... 代码
}, [treeData])

// 修改后
const stats = useMemo(() => {
  // ... 代码
}, [treeData.members])

步骤 2: 添加照片分组 useMemo

文件: app/page.tsx

位置: 在 allPhotos useMemo 之后添加

// 添加新的 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])

修改 JSX: 在照片展示部分(第 ~750-800 行)

// 修改前
{allPhotos.length > 0 ? (
  <div className="space-y-8">
    {(() => {
      const sortedPhotos = [...allPhotos].sort(...)
      const groupedByMonth: Record<string, typeof allPhotos> = {}
      // ... 分组逻辑
      return Object.entries(groupedByMonth).map(...)
    })()}
  </div>
) : (
  // ...
)}

// 修改后
{allPhotos.length > 0 ? (
  <div className="space-y-8">
    {groupedPhotosByMonth.map(([month, monthPhotos]) => (
      // ... 原有的 JSX
    ))}
  </div>
) : (
  // ...
)}

步骤 3: 修复 handleAdminPhotoToggle useCallback

文件: app/page.tsx

位置: 第 ~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])

// 修改后
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
  if (!isOwner) return
  
  const key = `${memberId}|${photoUrl}`
  setAdminToggleLoading(key)
  
  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])

第二阶段:中等优化(预期时间:2-3 小时)

步骤 4: 优化 stats 计算为单次遍历

文件: app/page.tsx

位置: 第 ~200-250 行

参考 OPTIMIZATION_EXAMPLES.md 中的"优化 4"部分,将多次遍历改为单次遍历。

关键改动:

  • 使用单个 forEach 循环替代多个 filtermap
  • 累积计算所有统计数据
  • 保持返回值结构不变

预期性能提升: 10-15%


步骤 5: 提取未来三月纪念日计算

文件: app/page.tsx

位置: 第 ~600-900 行

参考 OPTIMIZATION_EXAMPLES.md 中的"优化 2"部分。

关键步骤:

  1. 在组件外部定义 calculateEventDate 函数
  2. 在组件外部定义 createUpcomingEvent 函数
  3. 创建 upcomingEvents useMemo
  4. 修改 JSX 使用新的 useMemo

预期性能提升: 15-25%


步骤 6: 提取照片卡片为单独组件

文件: 新建 components/dashboard/photo-card.tsx

参考 OPTIMIZATION_EXAMPLES.md 中的"优化 5"部分。

关键步骤:

  1. 创建新文件 components/dashboard/photo-card.tsx
  2. 复制 PhotoCard 组件代码
  3. app/page.tsx 中导入并使用
  4. 删除原有的照片卡片 JSX

预期性能提升: 5-10%


第三阶段:高级优化(预期时间:4-6 小时)

步骤 7: 拆分大型组件

文件: 创建多个新文件

新建文件结构:

components/dashboard/
├── stats-section.tsx          # 统计概览
├── photos-tab.tsx             # 照片标签页
├── photo-gallery.tsx          # 照片库
├── photo-card.tsx             # 照片卡片(已创建)
├── recent-tab.tsx             # 动态标签页
├── anniversaries-section.tsx  # 纪念日部分
├── activity-log-section.tsx   # 活动日志部分
├── statistics-tab.tsx         # 统计图表标签页
├── migration-tab.tsx          # 籍贯标签页
└── location-groups.tsx        # 籍贯记录

步骤:

  1. 为每个部分创建单独的组件文件
  2. 将相关的 useMemo 和事件处理器移到对应的组件
  3. 通过 props 传递必要的数据和回调
  4. 在主组件中导入并组合这些子组件

预期性能提升: 20-30%


步骤 8: 实现虚拟滚动(可选)

文件: components/dashboard/photo-gallery.tsx

如果照片数量很多(>100),考虑使用虚拟滚动库:

npm install react-window

实现示例:

import { FixedSizeList as List } from 'react-window'

const PhotoGallery = ({ groupedPhotos }) => {
  return (
    <List
      height={600}
      itemCount={groupedPhotos.length}
      itemSize={35}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {/* 照片组件 */}
        </div>
      )}
    </List>
  )
}

预期性能提升: 30-50%(仅在照片数量很多时)


🧪 测试计划

单元测试

// tests/page.test.tsx
import { render, screen } from '@testing-library/react'
import DashboardPage from '@/app/page'

describe('DashboardPage Performance', () => {
  it('should render stats without unnecessary re-renders', () => {
    const { rerender } = render(<DashboardPage />)
    // 测试 stats 是否被正确 memoized
  })

  it('should group photos by month efficiently', () => {
    // 测试照片分组是否被正确 memoized
  })

  it('should calculate upcoming events efficiently', () => {
    // 测试未来事件计算是否被正确 memoized
  })
})

性能测试

// 使用 React DevTools Profiler
// 1. 打开 React DevTools
// 2. 切换到 Profiler 标签
// 3. 记录性能数据
// 4. 比较优化前后的性能指标

// 关键指标:
// - 组件渲染时间
// - 不必要的重新渲染次数
// - 内存使用量

集成测试

// 测试场景:
// 1. 初始加载 - 测量首次渲染时间
// 2. 添加成员 - 测量重新渲染时间
// 3. 删除成员 - 测量重新渲染时间
// 4. 更新照片权限 - 测量响应时间
// 5. 切换标签页 - 测量切换时间

📊 性能基准

优化前(基准)

指标
初始渲染时间 ~500ms
重新渲染时间 ~300ms
内存使用 ~50MB
不必要重新渲染 5-10次

优化后(目标)

指标 改进
初始渲染时间 ~350ms -30%
重新渲染时间 ~180ms -40%
内存使用 ~40MB -20%
不必要重新渲染 1-2次 -80%

🔍 验证清单

第一阶段完成后

  • 所有 useMemo 依赖项已修复
  • 照片分组逻辑已 memoized
  • handleAdminPhotoToggle useCallback 已修复
  • 没有 TypeScript 错误
  • 功能测试通过

第二阶段完成后

  • stats 计算已优化为单次遍历
  • 未来三月纪念日计算已提取
  • 照片卡片已提取为单独组件
  • 性能提升 10-15%
  • 功能测试通过

第三阶段完成后

  • 大型组件已拆分
  • 所有子组件已创建
  • 虚拟滚动已实现(如需要)
  • 性能提升 20-30%
  • 所有测试通过

🚀 部署步骤

  1. 创建特性分支

    git checkout -b feat/optimize-dashboard-page
    
  2. 实现优化

    • 按照上述步骤逐步实现
    • 每个步骤完成后提交一次
  3. 测试

    npm run test
    npm run build
    
  4. 性能测试

    • 使用 React DevTools Profiler
    • 对比优化前后的性能指标
  5. 代码审查

    • 提交 Pull Request
    • 等待代码审查
  6. 合并和部署

    git merge feat/optimize-dashboard-page
    npm run deploy
    

📚 参考资源