简单 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 未必是当前显示的图片,可能导致多张图片同时显示,或隐藏了错误的图片。
应当让切换串行执行:
- 加载并准备下一张图片。
- 到达切换时间后更新画面。
- 再准备后续图片。
加载失败时,应保留当前画面,并处理 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 无法实现这种播放效果。
备注:内容仅供参考。
