You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

4.4 KiB

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

变更内容

移除的逻辑

// ❌ 移除: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);
}

新增的逻辑

// ✅ 使用派生状态模式(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. 合并状态:将 urlloadederror 合并到一个对象中
  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 - 参考规范

参考资料