React应用轮播图图片未缓存,切换筛选重复下载问题求助
我正在开发一个展示个人照片的React应用,图片存储在本地public文件夹,通过utils目录下的JSON文件管理图片数据,图片分为photo和illustration两类,切换筛选条件时重新渲染对应图片。
使用map函数结合picture标签渲染筛选后的图片,但切换筛选时图片加载缓慢,检查网络面板发现图片每次都重新下载,而非读取内存缓存(已确认DevTools缓存未禁用,且图片已做优化)。尝试过懒加载依赖但无效,推测可能和渲染方式有关,但不确定具体原因。
JSON数据结构(editorialData)
{ "editorial": [ { "fallback": "images/photos/jpg/DSCF0369.jpg", "webp": "images/photos/webp/DSCF0369.webp", "title": "Photography", "date": "2022-09-01", "type": "photography", "category": "street" }, { "fallback": "images/photos/jpg/DSCF1325.jpg", "webp": "images/photos/webp/DSCF1325.webp", "title": "Photography", "date": "2022-09-24", "type": "photography", "category": "street" } ] }
核心代码片段
import editorialData from "../../../../utils/editorial.json" import { useDraggable } from "react-use-draggable-scroll" import { useEffect, useRef, useState } from "react" import ArrowReturn from "../../../../assets/icons/ArrowReturn" import { LazyLoadImage } from "react-lazy-load-image-component" export default function Editorial() { const photosRef = useRef(null) const photosTrackerRef = useRef() const [photoProgress, setPhotoProgress] = useState(0) const [radioState, setRadioState] = useState("photo") const { events: events1 } = useDraggable(photosRef, { safeDisplacement: 11, decayRate: 0.6, }) const maxWidth = (ref) => { if (ref) { return ref.scrollWidth - ref.clientWidth } } const windowWidth = window.innerWidth useEffect(() => { const calculateTrackerWidth = (ref, progress) => { return Math.round( (windowWidth / 100) * ((progress / maxWidth(ref)) * 100) ) } photosTrackerRef.current.style.setProperty( "--PhotosTrackerWidth", Math.round( (calculateTrackerWidth(photosRef.current, photoProgress) / windowWidth) * 100 ) + "%" ) }, [windowWidth, photoProgress]) function sortDesc(a, b) { return new Date(b.date).getTime() - new Date(a.date).getTime() } const filterOnRadioState = () => { let filteredImgs if (radioState === "photo") { filteredImgs = editorialData.editorial.filter( (img) => img.type === "photography" ) } else if (radioState === "illustration") { filteredImgs = editorialData.editorial.filter( (img) => img.type === "illustration" ) } return filteredImgs.sort(sortDesc) } useEffect(() => { const timer = setInterval(() => { if (photosRef) { setPhotoProgress(photosRef.current.scrollLeft) } }, 300) return () => clearInterval(timer) }, []) const onOptionChange = (e) => { setRadioState(e.target.value) photosRef.current.scrollLeft = 0 setPhotoProgress(0) } return ( <> <div ref={photosTrackerRef} className="photoTracker"></div> <div onClick={() => (photosRef.current.scrollLeft = 0)} className={`return_zero${photoProgress > 800 ? "" : " hidden"}`}> <ArrowReturn /> </div> <div className="radio_btn-wrapper"> <div className={`label_highlight${ radioState === "photo" ? "" : " highlight_right" }`}></div> <div className="radio_btn"> <input type="radio" name="radio" id="photo" value="photo" checked={radioState === "photo"} onChange={onOptionChange} /> <label className="photo_label" htmlFor="photo" style={ radioState === "photo" ? { color: "var(--fourthGrey)" } : null }> Photography </label> </div> <div className="radio_btn"> <input type="radio" name="radio" value="illustration" id="illustration" checked={radioState === "illustration"} onChange={onOptionChange} /> <label className="illustration_label" htmlFor="illustration" style={ radioState === "illustration" ? { color: "var(--fourthGrey)" } : null }> Illustration </label> </div> </div> <div className="photos_wrapper" ref={photosRef} {...events1}> {filterOnRadioState().map((props, index) => { return ( <picture className="photo_img"> <source type="image/webp" srcSet={props.webp}></source> <source type="image/jpeg" srcSet={props.fallback}></source> <LazyLoadImage key={index} type="image" className="editorial_img" src={props.url} alt={props.title}></LazyLoadImage> </picture> ) })} </div> </> ) }
1. 修复LazyLoadImage的src属性错误
代码中LazyLoadImage使用了props.url,但JSON数据里并没有url字段,这会导致图片加载失败,切换筛选时触发不必要的重新请求。应改为使用props.fallback作为src值:
<LazyLoadImage type="image" className="editorial_img" src={props.fallback} alt={props.title}></LazyLoadImage>
2. 优化React元素的key值
当前使用index作为key,切换筛选时列表重新排序/过滤,React会认为所有元素都是新的,销毁旧DOM节点后创建新节点——这会导致浏览器丢弃已缓存的图片,必须重新下载。
改用每个图片唯一的标识作为key,比如props.fallback(文件名唯一):
{filterOnRadioState().map((props) => { return ( <picture className="photo_img" key={props.fallback}> <source type="image/webp" srcSet={props.webp}></source> <source type="image/jpeg" srcSet={props.fallback}></source> <LazyLoadImage type="image" className="editorial_img" src={props.fallback} alt={props.title}></LazyLoadImage> </picture> ) })}
这样React会复用相同key的DOM节点,浏览器就能直接读取内存缓存里的图片。
3. 调整渲染策略:复用DOM节点,切换显示状态
如果图片数量不多,可以一次性渲染所有图片,通过CSS切换显示/隐藏,而非每次过滤后重新渲染列表:
// 提前处理排序后的所有图片,避免重复计算 const sortedImgs = useMemo(() => { return [...editorialData.editorial].sort(sortDesc) }, []) // 渲染时根据筛选状态控制显示 <div className="photos_wrapper" ref={photosRef} {...events1}> {sortedImgs.map((props) => { const shouldShow = radioState === "photo" ? props.type === "photography" : props.type === "illustration" return ( <picture className={`photo_img ${shouldShow ? "" : "hidden"}`} key={props.fallback}> <source type="image/webp" srcSet={props.webp}></source> <source type="image/jpeg" srcSet={props.fallback}></source> <LazyLoadImage type="image" className="editorial_img" src={props.fallback} alt={props.title}></LazyLoadImage> </picture> ) })} </div>
配合CSS:
.hidden { display: none; }
这种方式DOM节点始终存在,浏览器不会重新下载已缓存的图片,切换筛选时仅做显示/隐藏操作,性能更优。
4. 验证图片缓存响应头
确保服务器(或开发服务器)返回的图片响应头包含合理的缓存策略,比如Cache-Control: public, max-age=31536000,让浏览器长期缓存图片。可以在DevTools的Network面板中检查每个图片请求的Response Headers确认。
内容的提问来源于stack exchange,提问作者Arthur Robaeys

