# app/page.tsx 优化代码示例 ## 优化 1: 照片分组逻辑 ### ❌ 原始代码(问题) ```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(([month, monthPhotos]) => ( // ... JSX )) })()} ``` ### ✅ 优化后代码 ```typescript // 在组件顶部添加 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]) // 在 JSX 中使用 {allPhotos.length > 0 ? (
{groupedPhotosByMonth.map(([month, monthPhotos]) => ( // ... JSX ))}
) : ( // ... 空状态 )} ``` --- ## 优化 2: 未来三月纪念日计算 ### ❌ 原始代码(问题) ```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()) return ( // ... JSX ) })()} ``` ### ✅ 优化后代码 首先,提取辅助函数: ```typescript // 在组件外部定义 const calculateEventDate = ( dateStr: string, isLunar: boolean, now: Date ): { date: Date; lunarDisplay?: string } | null => { const date = new Date(dateStr) let eventDate: Date let lunarDisplay: string | undefined if (isLunar) { const lunarInfo = solar2lunar(date) if (lunarInfo) { const thisYearLunar = lunar2solar( now.getFullYear(), lunarInfo.lunarMonth, lunarInfo.lunarDay, lunarInfo.isLeap ) if (thisYearLunar) { eventDate = thisYearLunar lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}` } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } return { date: eventDate, lunarDisplay } } const createUpcomingEvent = ( member: any, type: 'birth' | 'death', dateStr: string, isLunar: boolean, now: Date, threeMonthsLater: Date ) => { const result = calculateEventDate(dateStr, isLunar, now) if (!result) return null const { date, lunarDisplay } = result if (date >= now && date <= threeMonthsLater) { return { member, type, date, originalDate: dateStr, isLunar, lunarDisplay, month: date.getMonth() + 1, day: date.getDate() } } return null } ``` 然后在组件中使用 useMemo: ```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 => { // 生日 if (member.birthDate) { const birthEvent = createUpcomingEvent( member, 'birth', member.birthDate, member.isLunarDate || false, now, threeMonthsLater ) if (birthEvent) events.push(birthEvent) } // 忌日 if (member.deathDate) { const deathEvent = createUpcomingEvent( member, 'death', member.deathDate, member.isLunarDate || false, now, threeMonthsLater ) if (deathEvent) events.push(deathEvent) } }) return events.sort((a, b) => a.date.getTime() - b.date.getTime()) }, [treeData.members]) // 在 JSX 中使用 const birthEvents = upcomingEvents.filter(e => e.type === 'birth') const deathEvents = upcomingEvents.filter(e => e.type === 'death') return upcomingEvents.length > 0 ? (
{/* 诞辰列 */}

诞辰 {birthEvents.length}

{birthEvents.length > 0 ? ( birthEvents.map((event, index) => ( )) ) : (

无诞辰

)}
{/* 忌日列 */}

忌日 {deathEvents.length}

{deathEvents.length > 0 ? ( deathEvents.map((event, index) => ( )) ) : (

无忌日

)}
) : (

来日无期

) ``` --- ## 优化 3: 修复 handleAdminPhotoToggle useCallback ### ❌ 原始代码(问题) ```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 导致每次都创建新函数 ``` ### ✅ 优化后代码 ```typescript 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]) // ✅ 移除 treeData.members 依赖 ``` --- ## 优化 4: 优化 stats 计算为单次遍历 ### ❌ 原始代码(问题) ```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 const birthYears = members .map(m => m.birthDate ? new Date(m.birthDate).getFullYear() : null) .filter(y => y !== null) as number[] const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear() const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0 const deceasedWithAge = members.filter(m => m.birthDate && m.deathDate) const totalAge = deceasedWithAge.reduce((sum, m) => { const birthYear = new Date(m.birthDate!).getFullYear() const deathYear = new Date(m.deathDate!).getFullYear() return sum + (deathYear - birthYear) }, 0) const averageLifespan = deceasedWithAge.length > 0 ? Math.round(totalAge / deceasedWithAge.length) : 0 return { totalMembers, livingMembers, deceasedMembers, maleCount, femaleCount, maxGeneration, yearsSpan, earliestYear, averageLifespan } }, [treeData]) ``` ### ✅ 优化后代码 ```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]) // ✅ 更精细的依赖项 ``` --- ## 优化 5: 提取照片卡片为单独组件 ### 新建文件:`components/dashboard/photo-card.tsx` ```typescript import React, { useCallback } from 'react' import Link from 'next/link' import { format } from 'date-fns' import { Switch } from '@/components/ui/switch' import { MemberNameWithStatus } from '@/components/member-name-with-status' import { Play, Video } from 'lucide-react' interface PhotoCardProps { photo: { url: string caption?: string uploadedAt: string memberId: string memberName: string isDead: boolean adminVisibleOverride: boolean visibleInOverview: boolean } isOwner: boolean currentTree?: { id?: string } onSelect: (url: string) => void onToggle: (memberId: string, photoUrl: string, value: boolean) => void isLoading: boolean } const isVideoFile = (url: string) => { const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'] return videoExtensions.some(ext => url.toLowerCase().endsWith(ext)) } export const PhotoCard: React.FC = ({ photo, isOwner, currentTree, onSelect, onToggle, isLoading }) => { const handleToggle = useCallback((checked: boolean) => { onToggle(photo.memberId, photo.url, checked) }, [photo.memberId, photo.url, onToggle]) if (photo.adminVisibleOverride === false) { return (
来自
{format(new Date(photo.uploadedAt), 'MM-dd')}
{isOwner && (
允许展示
)} {!isOwner && (

管理员已隐藏

)}
) } return (
{/* 媒体区域 */}
onSelect(photo.url)} > {isVideoFile(photo.url) ? (
) : ( {photo.caption )}
{/* 底部信息 */}
{/* 照片说明 */}
{photo.caption ? ( {photo.caption} ) : ( 暂无说明 )}
{/* 分享人和时间 */}
来自
{format(new Date(photo.uploadedAt), 'MM-dd')}
{/* 管理员权限开关 */} {isOwner && (
允许展示
)}
) } ``` ### 在主组件中使用 ```typescript import { PhotoCard } from '@/components/dashboard/photo-card' // 在 JSX 中 {groupedPhotosByMonth.map(([month, monthPhotos]) => (

{month} ({monthPhotos.length})

{monthPhotos.map((photo, index) => (
))}
))} ``` --- ## 优化 6: 修复所有 useMemo 依赖项 ### 检查清单 ```typescript // ❌ 不好的依赖项 const monthlyAnniversaries = useMemo(() => { // ... }, [treeData]) // 整个对象 // ✅ 好的依赖项 const monthlyAnniversaries = useMemo(() => { // ... }, [treeData.members]) // 只依赖需要的部分 // ❌ 不好的依赖项 const allPhotos = useMemo(() => { // ... }, [treeData, isOwner]) // treeData 是整个对象 // ✅ 好的依赖项 const allPhotos = useMemo(() => { // ... }, [treeData.members, isOwner]) // 只依赖需要的部分 // ❌ 不好的依赖项 const locationGroups = useMemo(() => { // ... }, [treeData]) // 整个对象 // ✅ 好的依赖项 const locationGroups = useMemo(() => { // ... }, [treeData.members]) // 只依赖需要的部分 ``` --- ## 性能测试建议 ### 使用 React DevTools Profiler ```typescript // 在浏览器控制台运行 import { Profiler } from 'react' // 包装组件 { console.log(`${id} (${phase}) took ${actualDuration}ms`) }}> ``` ### 测试场景 1. **初始加载**:测量首次渲染时间 2. **数据更新**:添加/删除成员后的重新渲染时间 3. **照片加载**:加载大量照片时的性能 4. **交互响应**:点击开关、展开折叠等操作的响应时间 ### 预期改进 - 初始加载时间:减少 20-30% - 重新渲染时间:减少 30-40% - 内存使用:减少 15-20%