requestAnimationFrame+canvas预加载帧序列是最佳逐帧动画方案,支持精确时序、动态控制与交互;img切换适用于轻量静态场景;须避免CSS动画及setInterval等不可控方式。

直接用 requestAnimationFrame 控制帧率 + canvas 或 img 序列切换,是当前最可控、兼容性最好的逐帧动画实现方式。CSS 动画或 GIF 无法精确控制帧时序,也不支持运行时动态跳帧或暂停。
用 canvas 绘制帧序列(推荐)
适合需要动态控制(如变速、倒放、跳转)、叠加图层或响应用户交互的场景。核心是预加载所有帧图像,再按需绘制到 canvas 上。
- 必须提前加载全部帧图片,避免绘制时出现空白或闪烁;可用
Promise.all(imgs.map(img => new Promise(...)))等待完成 - 帧索引从
0开始,每帧显示时长建议用毫秒(如100ms ≈ 10 FPS),不要依赖帧率“平均值” - 使用
requestAnimationFrame而非setTimeout,保证与屏幕刷新同步,减少卡顿 -
canvas.getContext('2d').drawImage()的裁剪参数要严格匹配单帧尺寸,否则会拉伸或错位
const frames = [/* 预加载的 Image 对象数组 */]; let currentFrame = 0; let lastTime = 0; const frameDuration = 100; // 每帧 100msfunction animate(timestamp) { if (timestamp - lastTime >= frameDuration) { currentFrame = (currentFrame + 1) % frames.length; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(frames[currentFrame], 0, 0); lastTime = timestamp; } requestAnimationFrame(animate); } requestAnimationFrame(animate);
用 ![如何制作逐帧HTML5动画_HTML5帧动画实现步骤【帧动画教程】]()
切换 src 实现(轻量级)
适合静态帧数不多(≤50)、无需复杂逻辑的展示型动画,资源复用率高,DOM 渲染开销低,但无法像素级控制。
- 所有帧图片应统一尺寸,并确保文件名/路径可按序号生成(如
frame_001.png→frame_050.png) -
浏览器可能对同一
src做缓存,但切换瞬间仍可能闪白——可提前创建Image对象并赋值img.src触发预加载 - 避免在循环中频繁设置
img.src,应在requestAnimationFrame回调里更新,否则可能被节流或丢帧 - 移动端 Safari 对快速
src切换有渲染延迟,建议最低帧间隔 ≥ 60ms
常见错误:用 CSS @keyframes 模拟逐帧
看似简洁,实则隐患多:帧数一多(>20)会导致 CSS 文件体积激增;无法动态修改帧序;Safari 中 steps() 在缩放或 transform 下易出现半帧渲染;且不支持运行时暂停/跳转。
立即学习“前端免费学习笔记(深入)”;
-
animation-timing-function: steps(1, start)必须与总帧数、总时长严格对应,算错一个参数就全乱 - 每帧用
background-position移动雪碧图时,若雪碧图宽高不是单帧整数倍,会出现模糊或错位 - Chrome 115+ 对超长
steps(N)的解析有性能警告,实际帧率可能低于预期 - 没有 JS 接口,无法监听“当前播放到第几帧”,后续扩展(如音画同步)几乎不可行
关键细节容易被忽略
逐帧动画的“准”比“快”重要。很多问题不是代码写错,而是时间基准没对齐:
- 别用
setInterval驱动帧逻辑——它不感知页面是否隐藏、屏幕是否刷新,后台标签页中会严重失步 - 帧计时要用
performance.now()或requestAnimationFrame的timestamp,不用Date.now() - 预加载失败的帧必须有 fallback 处理(比如跳过、占位灰块、报错日志),否则整个动画卡死
- 移动端要注意
canvas尺寸单位:CSS width/height ≠ 绘图分辨率,需显式设置canvas.width和canvas.height










