446 lines
10 KiB
Markdown
446 lines
10 KiB
Markdown
# app/page.tsx 优化实现指南
|
|
|
|
## 📋 实现步骤
|
|
|
|
### 第一阶段:快速修复(预期时间:1-2 小时)
|
|
|
|
#### 步骤 1: 修复 useMemo 依赖项
|
|
|
|
**文件**: `app/page.tsx`
|
|
|
|
**修改位置 1**: 第 ~280-290 行 - recentMembers
|
|
```typescript
|
|
// 修改前
|
|
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
|
|
```typescript
|
|
// 修改前
|
|
const allPhotos = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData, isOwner])
|
|
|
|
// 修改后
|
|
const allPhotos = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData.members, isOwner])
|
|
```
|
|
|
|
**修改位置 3**: 第 ~400-500 行 - monthlyAnniversaries
|
|
```typescript
|
|
// 修改前
|
|
const monthlyAnniversaries = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData])
|
|
|
|
// 修改后
|
|
const monthlyAnniversaries = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData.members])
|
|
```
|
|
|
|
**修改位置 4**: 第 ~550-570 行 - locationGroups
|
|
```typescript
|
|
// 修改前
|
|
const locationGroups = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData])
|
|
|
|
// 修改后
|
|
const locationGroups = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData.members])
|
|
```
|
|
|
|
**修改位置 5**: 第 ~200-250 行 - stats
|
|
```typescript
|
|
// 修改前
|
|
const stats = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData])
|
|
|
|
// 修改后
|
|
const stats = useMemo(() => {
|
|
// ... 代码
|
|
}, [treeData.members])
|
|
```
|
|
|
|
---
|
|
|
|
#### 步骤 2: 添加照片分组 useMemo
|
|
|
|
**文件**: `app/page.tsx`
|
|
|
|
**位置**: 在 `allPhotos` useMemo 之后添加
|
|
|
|
```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])
|
|
```
|
|
|
|
**修改 JSX**: 在照片展示部分(第 ~750-800 行)
|
|
```typescript
|
|
// 修改前
|
|
{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 行
|
|
|
|
```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])
|
|
|
|
// 修改后
|
|
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` 循环替代多个 `filter` 和 `map`
|
|
- 累积计算所有统计数据
|
|
- 保持返回值结构不变
|
|
|
|
**预期性能提升**: 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),考虑使用虚拟滚动库:
|
|
|
|
```bash
|
|
npm install react-window
|
|
```
|
|
|
|
**实现示例**:
|
|
```typescript
|
|
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%(仅在照片数量很多时)
|
|
|
|
---
|
|
|
|
## 🧪 测试计划
|
|
|
|
### 单元测试
|
|
```typescript
|
|
// 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
|
|
})
|
|
})
|
|
```
|
|
|
|
### 性能测试
|
|
```typescript
|
|
// 使用 React DevTools Profiler
|
|
// 1. 打开 React DevTools
|
|
// 2. 切换到 Profiler 标签
|
|
// 3. 记录性能数据
|
|
// 4. 比较优化前后的性能指标
|
|
|
|
// 关键指标:
|
|
// - 组件渲染时间
|
|
// - 不必要的重新渲染次数
|
|
// - 内存使用量
|
|
```
|
|
|
|
### 集成测试
|
|
```typescript
|
|
// 测试场景:
|
|
// 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. **创建特性分支**
|
|
```bash
|
|
git checkout -b feat/optimize-dashboard-page
|
|
```
|
|
|
|
2. **实现优化**
|
|
- 按照上述步骤逐步实现
|
|
- 每个步骤完成后提交一次
|
|
|
|
3. **测试**
|
|
```bash
|
|
npm run test
|
|
npm run build
|
|
```
|
|
|
|
4. **性能测试**
|
|
- 使用 React DevTools Profiler
|
|
- 对比优化前后的性能指标
|
|
|
|
5. **代码审查**
|
|
- 提交 Pull Request
|
|
- 等待代码审查
|
|
|
|
6. **合并和部署**
|
|
```bash
|
|
git merge feat/optimize-dashboard-page
|
|
npm run deploy
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 参考资源
|
|
|
|
- [React useMemo 文档](https://react.dev/reference/react/useMemo)
|
|
- [React useCallback 文档](https://react.dev/reference/react/useCallback)
|
|
- [React DevTools Profiler](https://react.dev/learn/react-developer-tools)
|
|
- [Web Vitals](https://web.dev/vitals/)
|
|
- [React 性能优化](https://react.dev/learn/render-and-commit)
|
|
|