You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 09:50:02