requestAnimationFrame 不是定时器,掉帧主因是误当 setInterval 使用;正确做法是用其时间戳计算 deltaTime、仅操作合成属性、避免重排与同步阻塞。

requestAnimationFrame 不是“更高级的 setInterval”,它根本就不是定时器——用错思路,动画照样掉帧、卡顿、跨设备表现不一致。
为什么 requestAnimationFrame 会掉帧?
掉帧不是因为函数写错了,而是你把它当成了 setInterval 的替代品来“轮询”:比如在回调里固定加 this.x += 2,再手动控制间隔。这完全违背了它的设计逻辑。
真实掉帧原因有三个:
- 没传时间戳参数,导致无法计算帧间耗时(
deltaTime),位移量无法随设备刷新率自适应 - 在回调中执行了重排(reflow)操作,比如反复读取
offsetLeft或修改非合成层样式(如left、top) - 动画函数内做了大量同步计算或 DOM 插入,阻塞了下一帧的准备时机
怎么写一个真正稳帧的 animate 函数?
核心就两件事:用好时间戳 + 控制渲染路径。下面是最小可用模板:
立即学习“前端免费学习笔记(深入)”;
let lastTime = 0;
function animate(currentTime) {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
<p>// ✅ 用 deltaTime 做时间敏感更新(比如每秒移动 120px)
ball.x += (120 / 1000) * deltaTime;</p><p>// ✅ 渲染只做 transform + opacity(触发合成层)
ballEl.style.transform = <code>translateX(${ball.x}px)</code>;</p><p>// ✅ 下一帧继续
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);注意几个关键点:
- 必须接收
currentTime参数,这是浏览器传入的真实高精度时间戳(单位 ms) - 不要用
Date.now()或performance.now()自己取,它们和 rAF 不对齐 - 启动只需一次
requestAnimationFrame(animate),不是animate()再调一次 - 别在动画函数里调
console.log或做 layout 触发操作(如getBoundingClientRect)
requestAnimationFrame 和 canvas 动画怎么配合?
canvas 动画最容易出问题的地方是“清画布”和“绘制顺序”。常见错误是每帧都全量重绘,或者漏清残留图像。
正确做法:
- 每帧开头必须调用
ctx.clearRect(0, 0, canvas.width, canvas.height),否则上一帧内容会残留 - 如果只动局部图形(比如一个小球),可以用
ctx.save()/ctx.restore()配合ctx.beginPath()精确控制绘制范围 - 避免在
animate中频繁创建新路径或对象(如每次 new Path2D()),复用已有实例 - canvas 尺寸要用
canvas.width/canvas.height设置,别只靠 CSS 缩放,否则模糊+性能降级
cancelAnimationFrame 容易被忽略的细节
很多人以为只要存下 requestAnimationFrame 返回的 ID 就能随时停掉动画,但有两个坑:
- ID 是一次性有效的:调用
cancelAnimationFrame(id)后,该 ID 失效;再次requestAnimationFrame会返回新 ID,旧 ID 不再起作用 - 没清理 ID 变量会导致内存泄漏:比如组件卸载后,动画还在后台跑,ID 还挂在闭包里 —— 必须显式设为
null或用 WeakMap 管理 - 页面不可见时,
requestAnimationFrame会自动暂停(符合规范),但 resume 时不保证时间戳连续,所以deltaTime计算要容错(比如限制最大值为 100ms)
真正可靠的启停模式是封装成状态机,而不是靠裸 ID 控制。



















