
本文详解如何通过预加载全部帧图像并避免动态切换 img.src 引发的竞态条件,彻底解决透明 png 在 canvas 中逐帧播放时出现的瞬时空白、闪烁和卡顿问题。
在基于滚动触发的 canvas 动画中(如产品展示页的帧动画),若采用“按需设置 img.src + drawImage”的旧模式,极易因图像未就绪导致渲染空白——这是因为 img.onload 是异步事件,而 drawImage 在图像尚未加载完成时会静默失败(不报错,但不绘制),造成几毫秒的全透明“闪帧”,尤其在 chrome 等浏览器中表现明显。
根本原因并非 clearRect 或 drawImage 性能瓶颈,而是资源加载与渲染逻辑的竞态:每次滚动都重新赋值 img.src,但未等待其加载完成就立即绘制,导致大量无效绘制调用。
✅ 正确解法是:预加载所有帧为已就绪的 Image 实例,并缓存于数组中,确保每一帧调用 drawImage 时,图像资源 100% 可用。
以下是优化后的完整实现:
const canvas = document.getElementById("prodPicAnimation"); const ctx = canvas.getContext("2d"); const frameCount = 120; // 生成全部帧 URL(注意:索引从 1 开始,补零至 3 位) const imageUrls = Array.from({ length: frameCount }, (_, i) => `https://www.dr-adler.com/content/produkte/Sheabutter/renderAnimation/${(i + 1).toString().padStart(3, '0')}.png` ); // 异步预加载单张图,返回已加载完成的 Image 对象 const preloadImage = (src) => { return new Promise(resolve => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => resolve(null); // 防止某帧失败阻塞整体 img.src = src; }); }; // 并行预加载全部帧,返回 Image 实例数组 const preloadAllFrames = () => Promise.all(imageUrls.map(preloadImage)); // 初始化动画系统 const init = async () => { console.log('开始预加载...'); const images = await preloadAllFrames(); // 过滤掉加载失败的帧(可选:替换为占位图或跳过) const validImages = images.filter(img => img !== null); if (validImages.length < frameCount) { console.warn(`警告:仅成功加载 ${validImages.length}/${frameCount} 帧`); } // 设置画布尺寸(建议在 CSS 中固定,此处确保像素精准) canvas.width = 750; canvas.height = 750; // 首帧立即渲染 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(validImages[0], 0, 0); // 绑定滚动监听:根据滚动位置计算目标帧索引,并 requestAnimationFrame 安全更新 window.addEventListener('scroll', () => { const scrollTop = document.documentElement.scrollTop; const maxScrollTop = 750; const scrollFraction = Math.max(0, Math.min(1, scrollTop / maxScrollTop)); const frameIndex = Math.min(validImages.length - 1, Math.floor(scrollFraction * validImages.length)); requestAnimationFrame(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(validImages[frameIndex], 0, 0); }); }); console.log('动画初始化完成,共加载', validImages.length, '帧'); }; // 启动 init();
? 关键优化点说明:
- ✅ 无竞态:所有 Image 对象在动画启动前已完成加载,drawImage 调用即刻生效;
- ✅ 零延迟切换:滚动时仅做数组索引访问与绘制,无网络 I/O 或解码开销;
- ✅ 内存友好:现代浏览器对已解码图像复用高效,120 张 750×750 PNG(平均 ~100KB)约占用 10–15MB 内存,完全可控;
- ✅ 健壮性增强:加入 onerror 处理,避免单帧失败导致白屏;
- ✅ 渲染安全:严格使用 requestAnimationFrame 包裹绘制,确保与屏幕刷新率同步。
? 进阶建议:
- 若首屏加载时间敏感,可添加加载状态 ui(如
加载中…
),并在 init() 结束后移除;
- 对超大帧数(>200)或高分辨率场景,可考虑 Web Worker 预解码 + createImageBitmap 进一步提升首帧绘制速度;
- 避免在滚动回调中重复创建 Image 或触发 GC,本方案全程复用预加载对象,杜绝内存泄漏风险。
遵循此模式,即可实现丝滑、稳定、无闪烁的 Canvas 帧动画体验,完美适配产品页、交互式广告等高性能视觉场景。