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
4.4 KiB
MediaCard 性能优化
日期: 2026-02-04
类型: 性能优化
组件: client/src/components/common/MediaCard.tsx
背景
原 MediaCard 组件在图片加载状态管理上存在以下问题:
- 副作用在渲染中执行:使用
useMemo(() => new Image())探测浏览器缓存,违反 React 最佳实践 - 渲染函数中调用 setState:通过
if (imageUrl !== prevUrl) { setState(...) }重置状态,可能导致额外重渲染 - 不必要的复杂性:手动缓存探测逻辑复杂,且收益有限(浏览器原生缓存已足够快)
参考:/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) 是一种优雅的状态管理方案:
- 合并状态:将
url、loaded、error合并到一个对象中 - URL 绑定:每次更新状态时同时记录对应的 URL
- 自动失效:当 prop
imageUrl变化时,通过比较imageState.url !== currentUrl自动判定旧状态失效 - 零副作用:无需
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 警告
测试建议
- 快速点击切换图片:验证加载/错误状态正确重置
- 已缓存图片:确认短暂骨架屏不影响体验
- 错误图片:验证错误占位符正常显示
- 选中态、hover、overlay:确认交互效果不受影响
相关文件
client/src/components/common/MediaCard.tsx- 主要变更/react-best-practices- 参考规范