PAYATHON 2026

简单 slide-show 在 Firefox 中切换图片时闪烁,如何解决?

排障赵师傅

可以不用外部库。先调整异步切换的调度方式,再尝试给 <img> 设置 decoding="sync"。如果等待 decode() 后仍然闪烁,可以改用固定的 <canvas>:下一张图片准备好后覆盖当前画面,加载期间保留上一帧。

仅凭这些代码,还无法确认是 Firefox 的 bug。使用 decode() 的方向没错,但还需要排查请求完成顺序、页面布局和具体浏览器环境的影响。

为什么等待 onload 仍可能闪烁?

load 事件表示图片资源已成功加载,不代表图片已经绘制到屏幕上。图片显示还涉及解码和页面绘制。

浏览器可以异步解码图片,先显示页面上的其他内容,稍后再补上图片。如果上一张图片此时已经隐藏,就可能短暂出现空白。HTML 标准对图片解码的说明区分了同步和异步解码的呈现行为。

img.decode() 更适合这里的需求:它返回的 Promise 会在图片解码完成、数据可用后兑现。因此,等待 decode() 后仍然闪烁,不能简单归因于“图片还没解码”。不过,decode() 也不表示屏幕已经完成绘制。decode() 文档

连续修改两条 visibility 样式,也不意味着浏览器一定会在两条语句之间绘制空白画面。只调换“显示下一张”和“隐藏上一张”的代码顺序,通常不足以解决问题。

先检查现有实现

尝试同步解码提示

可以在图片元素上设置:

<img id="pic1" decoding="sync" alt="">

也可以在赋值 src 前设置:

img.decoding = "sync";

sync 表示希望图片与其他内容一起呈现,避免异步解码时图片暂时不显示。它只是给浏览器的提示,不能保证消除所有闪烁,也不会让下载变成同步操作。decode() 本身不受这个属性控制,两者不是同一个开关。HTML 标准中的解码规则

避免加载回调乱序

原代码每隔 150 ms 就启动下一次加载,不会等待上一次切换完成。如果某张图片加载较慢,后面的图片可能先加载完成,回调的执行顺序就会与图片索引不一致。

这时,回调中保存的 previous 未必是当前显示的图片,可能导致多张图片同时显示,或隐藏了错误的图片。

应当让切换串行执行:

  1. 加载并准备下一张图片。
  2. 到达切换时间后更新画面。
  3. 再准备后续图片。

加载失败时,应保留当前画面,并处理 error 或 Promise rejection。将 visibility 换成 display 无法解决这些调度问题,display: none 还可能影响页面布局。

使用固定的 canvas 更新画面

如果 <img> 显隐切换在目标 Firefox 环境中仍然闪烁,可以尝试用 <canvas> 绕开这条显示路径。

让画布始终可见,图片在页面之外加载并解码。准备完成后,用一次 drawImage() 覆盖旧画面。等待期间不清空画布,就不会主动露出背景。

下面的示例适用于不透明的 JPEG 图片,目标切换间隔为 125 ms,约为 8 FPS。使用时替换图片列表和画布尺寸即可。

<canvas
  id="slideshow"
  width="800"
  height="450"
  role="img"
  aria-label="图片序列">
  图片序列展示
</canvas>

<script>
const canvas = document.getElementById("slideshow");
const ctx = canvas.getContext("2d");

const urls = [
  "out1.jpg",
  "out2.jpg",
  "out3.jpg"
];

const interval = 1000 / 8;

const sleep = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

const nextFrame = () =>
  new Promise(resolve => requestAnimationFrame(resolve));

async function prepareImage(url) {
  const img = new Image();
  img.src = url;
  await img.decode();
  return img;
}

async function play() {
  if (!ctx || urls.length === 0) {
    return;
  }

  let lastDraw = -Infinity;

  for (let i = 0; ; i = (i + 1) % urls.length) {
    // 准备下一张时,画布继续显示当前图片。
    const img = await prepareImage(urls[i]);

    const remaining =
      interval - (performance.now() - lastDraw);

    if (remaining > 0) {
      await sleep(remaining);
    }

    await nextFrame();

    // JPEG 不透明,完整覆盖画布,无须先 clearRect()。
    ctx.drawImage(
      img,
      0,
      0,
      canvas.width,
      canvas.height
    );

    lastDraw = performance.now();
  }
}

play().catch(error => {
  // 停止播放并保留最后一次成功绘制的画面。
  console.error("图片播放失败:", error);
});
</script>

这个实现每次只准备下一张图片,切换顺序不会受并发请求完成顺序的影响。如果加载或解码耗时超过 125 ms,当前图片就会多显示一会儿,实际帧率也会降低。

requestAnimationFrame() 让更新接近浏览器的绘制时机,但不保证图片已经加载,也不保证稳定达到 8 FPS。

流畅度与 CPU 占用的注意事项

  • 图片尺寸应尽量接近实际显示尺寸,避免每帧都解码、缩放远大于显示区域的图片。示例会将图片拉伸到整个画布,实际使用时应保持图片与画布的宽高比一致。
  • 不要每帧重设 canvas.width 或 canvas.height。修改画布尺寸会重置位图和绘图状态,只应在初始化或确实需要调整时设置。Canvas 标准
  • 上述覆盖方式适用于不透明图片。透明 PNG 可能露出上一帧,需要在同一次绘制处理中先填充背景,再绘制图片。
  • 正式播放时使用稳定的图片 URL,以便复用缓存。随机 query-string 可以辅助测试,但会减少缓存复用的机会。
  • Canvas 仍需解码和绘制,不一定比 <img> 更省 CPU。应使用实际图片尺寸,在目标 Firefox 和设备上比较性能。不需要更新画面时,也不必持续重绘。

如果修正调度后,等待 decode() 的 <img> 方案仍能稳定复现空白闪烁,可以进一步调查 Firefox 的绘制行为。提交问题时应附上完整的 HTML、CSS、图片和浏览器环境。现有代码片段还不足以确认具体缺陷,也不能据此认定 Firefox 无法实现这种播放效果。

备注:内容仅供参考。