# MediaCard 性能优化 **日期**: 2026-02-04 **类型**: 性能优化 **组件**: `client/src/components/common/MediaCard.tsx` ## 背景 原 `MediaCard` 组件在图片加载状态管理上存在以下问题: 1. **副作用在渲染中执行**:使用 `useMemo(() => new Image())` 探测浏览器缓存,违反 React 最佳实践 2. **渲染函数中调用 setState**:通过 `if (imageUrl !== prevUrl) { setState(...) }` 重置状态,可能导致额外重渲染 3. **不必要的复杂性**:手动缓存探测逻辑复杂,且收益有限(浏览器原生缓存已足够快) 参考:`/react-best-practices` - 避免在渲染函数中执行副作用和 setState ## 变更内容 ### 移除的逻辑 ```typescript // ❌ 移除:useMemo 中的副作用 const isImageCached = useMemo(() => { if (!imageUrl) return false; const img = new Image(); img.src = imageUrl; return img.complete && img.naturalHeight !== 0; }, [imageUrl]); // ❌ 移除:渲染函数中的 setState const [prevUrl, setPrevUrl] = useState(imageUrl); if (imageUrl !== prevUrl) { setPrevUrl(imageUrl); setImageLoaded(isImageCached); setImageError(false); } ``` ### 新增的逻辑 ```typescript // ✅ 使用派生状态模式(Derived State Pattern) const [imageState, setImageState] = useState(() => ({ url: imageUrl ?? null, loaded: false, error: false, })); // 通过比较 URL 自动判断状态是否有效 const currentUrl = imageUrl ?? null; const isCurrentImage = imageState.url === currentUrl; const imageLoaded = isCurrentImage ? imageState.loaded : false; const imageError = isCurrentImage ? imageState.error : false; // 事件处理器同时更新 URL 和状态 const handleImageLoad = () => { setImageState({ url: currentUrl, loaded: true, error: false }); }; const handleImageError = () => { setImageState({ url: currentUrl, loaded: false, error: true }); }; ``` ## 技术方案 **派生状态模式 (Derived State Pattern)** 是一种优雅的状态管理方案: 1. **合并状态**:将 `url`、`loaded`、`error` 合并到一个对象中 2. **URL 绑定**:每次更新状态时同时记录对应的 URL 3. **自动失效**:当 prop `imageUrl` 变化时,通过比较 `imageState.url !== currentUrl` 自动判定旧状态失效 4. **零副作用**:无需 `useEffect`,无需渲染期间 setState,完全符合 React 最佳实践 **优势**: - ✅ 避免在 `useEffect` 中同步调用 setState(避免级联渲染) - ✅ 避免在渲染函数中调用 setState(避免额外重渲染) - ✅ 状态与 URL 强绑定,逻辑清晰 - ✅ 自动处理快速切换图片的场景 ## 性能提升 | 优化项 | 提升效果 | |--------|---------| | 避免每次渲染创建 `Image` 对象 | ✅ 减少内存分配和垃圾回收压力 | | 避免 `useEffect` 中的级联 setState | ✅ 消除 ESLint 警告,遵循 React 最佳实践 | | 简化状态管理逻辑 | ✅ 提升代码可维护性和可读性 | | 依赖浏览器原生缓存 | ✅ 利用 HTTP 缓存和 `loading="lazy"` 优化 | ## 行为变化 **变更前**: - 浏览器已缓存的图片:首次渲染几乎不显示骨架屏(通过 `new Image()` 探测) **变更后**: - 浏览器已缓存的图片:首次渲染可能短暂显示骨架屏(< 50ms) - 其他交互和视觉效果保持一致 **影响评估**: - 用户几乎无感知(浏览器缓存加载速度 < 50ms) - 换来更简洁、符合 React 最佳实践的代码 ## 符合的最佳实践 ✅ **React Best Practices**: - 使用派生状态模式管理依赖 prop 的状态 - 避免在 `useEffect` 中同步调用 setState - 避免在渲染函数中直接调用 setState - 简化状态管理逻辑 ✅ **代码质量**: - 更少的代码行数 - 更清晰的状态流转 - 更易于测试和维护 - 无 ESLint 警告 ## 测试建议 1. **快速点击切换图片**:验证加载/错误状态正确重置 2. **已缓存图片**:确认短暂骨架屏不影响体验 3. **错误图片**:验证错误占位符正常显示 4. **选中态、hover、overlay**:确认交互效果不受影响 ## 相关文件 - `client/src/components/common/MediaCard.tsx` - 主要变更 - `/react-best-practices` - 参考规范 ## 参考资料 - [React Docs: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) - [React Docs: Deriving State from Props](https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html)