图片预解码缓存

一、 一张图从 URL 到屏幕,中间发生了什么?

浏览器或客户端拿到图片后,大致会经历:

  1. 下载:从网络或磁盘读到压缩数据(JPEG/PNG/WebP 等)
  2. 解码(decode):把压缩数据解压成位图(像素数据),才能交给 GPU 合成、绘制
  3. 上传 / 合成:把位图交给渲染管线,和页面一起画出来用户常说的「卡顿」,很多时候出在 第 2 步解码:解码是 CPU 密集型工作,若在首帧要显示的那一瞬间才解码,主线程可能被占住,出现掉帧、白屏、闪烁。

二、 什么是「预解码」?

预解码 = 在图片真正要出现在视口里之前,提前完成 decode,把「压缩数据 → 位图」这一步做完。

典型场景:列表里下一屏的头像、封面,轮播下一张,路由切换前,提前加载详情页大图。

目标:显示时只做合成,不再临时 decode,首帧更稳。


三、 什么是「缓存」?和预解码怎么配合?

这里的「缓存」通常指:把已经 decode 好的位图(或可直接绘制的对象)留在内存里,下次再用同一张图时,跳过 decode。

浏览器通常按 URL(+ 缓存策略) 复用底层资源:

  • 网络层:HTTP 缓存,同 URL 不必再下
  • 解码层:同 URL 的位图可能已在内存里,可见 <img> 再绑这个 URL 时更快

可以粗分为几层(不同平台命名不一,思想类似):

层级 存什么 作用
HTTP / 磁盘缓存 压缩文件字节 省下载,不解码
内存中的压缩缓冲 原始 image 数据 少一次 IO,仍可能要 decode
解码后缓存(decoded cache) 解压后的位图 / 纹理 省 decode,占内存大

预解码 + 缓存 的组合就是:

提前 decode → 把结果放进 decoded cache → 需要显示时直接取用。


四、img.decode() / createImageBitmap 是什么?
  • img.decode()
    • 作用:在你真正把图片显示出来前,先让浏览器完成解码(decoded cache)。
    • HTMLImageElement 的方法。
TypeScript 复制代码
const img = new Image();
img.src = url;
await img.decode(); // 解码完成

createImageBitmap(...)

  • 把图片源(Blob/Image/Canvas 等)变成 ImageBitmap
  • ImageBitmap 是更"渲染友好"的位图对象,常用于 Canvas、OffscreenCanvas。
  • 好处:通常解码路径更高效、可异步,适合大图/频繁渲染场景。
TypeScript 复制代码
const bitmap = await createImageBitmap(blob);
// canvas.drawImage(bitmap, ...)

或者

const bitmap = await createImageBitmap(img);
ctx.drawImage(bitmap, 0, 0);

简单记:

decode() 更偏 <img> 展示链路优化;

createImageBitmap() 更偏 Canvas/位图渲染链路优化。


📌 示例:
TypeScript 复制代码
// 缓存:同一资源只 decode/create 一次
  // key: src
  const decodedCacheRef = useRef<Map<string, Promise<void>>>(new Map());
  const bitmapCacheRef = useRef<Map<string, Promise<ImageBitmap>>>(new Map());


  async function preloadWithDecode(src: string) {
    if (!decodedCacheRef.current.has(src)) {
      const task = (async () => {
        const img = new Image();
        img.src = src;
        await img.decode();
      })();
      decodedCacheRef.current.set(src, task);
    }
    await decodedCacheRef.current.get(src);
  }


  async function preloadWithBitmap(src: string) {
    // 实际可二选一:decode 或 bitmap。这里演示 bitmap 缓存思路。
    if (!bitmapCacheRef.current.has(src)) {
      const task = (async () => {
        const res = await fetch(src);
        const blob = await res.blob();
        // async 函数执行完毕后,返回值会自动包装成 Promise。
        return await createImageBitmap(blob);
      })();
      bitmapCacheRef.current.set(src, task);
    }
    await bitmapCacheRef.current.get(src);
  }

  // 预热全部资源(演示用;真实项目可只预热相邻档位)
  useEffect(() => {
    RESOURCES.forEach((r) => {
      preloadWithDecode(r.src).catch(console.error);
      preloadWithBitmap(r.src).catch(console.error);
    });
  }, []);

  // 缩放中不切图;仅在"停止移动后"延迟切换
  useEffect(() => {
    if (isMoving) return;
    if (timerRef.current) {
      window.clearTimeout(timerRef.current);
    }
    timerRef.current = window.setTimeout(async () => {
      if (targetLevel === activeLevel) return;
      const next = RESOURCES.find((r) => r.level === targetLevel);
      if (!next) return;
      // 切换前保证它已经解码好(不卡显示)
      await preloadWithDecode(next.src);
      setDisplaySrc(next.src);
      setActiveLevel(targetLevel);
    }, SWITCH_DELAY_MS);
    return () => {
      if (timerRef.current) window.clearTimeout(timerRef.current);
    };
  }, [isMoving, targetLevel, activeLevel]);

流程是:

  1. RESOURCES 找到目标档位 next
  2. next.srcdecodeCache
    • 有:复用缓存中
    • 无:新建 Promise 并存入缓存
  3. await 完成后,再 setDisplaySrc(next.src)

五、 为什么值得做?代价是什么?

收益

  • 减少滚动、切页时的 jank(卡顿)
  • 大图、多图列表体验更顺滑
  • 动画、转场时减少「闪一下再清晰」

代价

  • 内存:解码后体积 ≈ 宽 × 高 × 每像素字节数(例如 RGBA 约 4 字节/像素),一张 2000×2000 的图就可能十几 MB
  • CPU / 电量:预解码本身要花时间,预太多会抢主线程或其它任务
  • 策略复杂:缓存多大、何时淘汰(LRU)、弱网/低内存设备要不要关,都要设计

所以这不是「越多越好」,而是 在合适时机、对合适图片、用有上限的缓存。


六、 常见实现思路(跨平台概念)
Web
  1. HTMLImageElement.decode()(或 createImageBitmap):在显示前异步 decode,避免 decode 和首帧绘制挤在同一帧。
  2. <link rel="preload" as="image">:更早开始下载,不等于 decode,但常和预解码一起用。
  3. 浏览器内部往往还有 decoded image cache:同 URL、同尺寸的图片 decode 一次后复用(具体由引擎管理,开发者不能直接完全控制)。
移动端(iOS / Android)
  1. 系统图片库(如 UIImage、Bitmap、Coil/Glide)常有 内存缓存里分「原始数据」和「已解码 Bitmap」。
  2. 列表滚动库常做:即将进入屏幕的 item 提前 decode + 滑出屏幕的释放。

七、 和「预加载」的区别
概念 主要解决
预加载(preload / prefetch) 更早下载数据
预解码(pre-decode) 更早解压成位图
解码缓存 同一张图不要重复 decode

只预加载不解码:网络好了,但第一次显示仍可能卡一下。

只缓存不预解码:第二次快,第一次仍可能卡。

三者常一起用:preload → pre-decode → 放进 decoded cache → 显示。


八、 实践里常见的策略(原则)
  1. 只预解码「高概率会显示」的图(下一张、下一屏、hover 预览),避免内存爆炸。
  2. 控制并发:同时 decode 太多张会抢 CPU。
  3. 按尺寸 decode:显示只要 200px 宽,就不要先 decode 4000px 原图再缩小(很多库支持 sampleSize / resize 后再缓存)。
  4. 有淘汰策略:LRU、内存警告时清空 decoded cache,保留磁盘缓存即可。
  5. 弱网 / 低内存设备可降级:只做下载缓存,不做或少量预解码。

九、 一句话总结

图片预解码缓存 = 在需要显示之前就把图片从压缩格式解压成位图,并把解压结果留在内存里复用,从而用更多内存换更少的显示瞬间卡顿;需要和预加载、尺寸控制、缓存上限一起设计,不能无脑全开。

相关推荐
郑洁文3 小时前
基于网络爬虫的Web敏感信息泄露自动化检测工具
前端·爬虫·网络安全·自动化
郑洁文3 小时前
可视化Web渗透分析工具的设计与实现
前端
罗超驿3 小时前
18.Web API 实战:元素与表单属性的获取和修改
开发语言·前端·javascript
边界条件╝4 小时前
微前端进阶(四)
前端·状态模式
无风听海4 小时前
JSON Web Token(JWT)完全指南
java·前端·json
IT_陈寒4 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒4 小时前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
H0r1zon.4 小时前
PinCopy:双击 Ctrl,把剪贴板「钉」在屏幕上
前端
kyriewen5 小时前
大厂面试新规:不会用AI编程,直接挂
前端·面试·ai编程