requestAnimationFrame 比 setInterval 更适合动画,因其对齐屏幕刷新周期(约16.6ms)、自动节流、适配后台暂停与节能降频;setInterval 则机械触发,易致跳帧、卡顿、撕裂,且需手动处理稳定性与清理。

requestAnimationFrame 为什么比 setInterval 更适合动画
浏览器的 requestAnimationFrame 是为动画量身定制的调度机制,它会把回调对齐到屏幕刷新周期(通常是每 16.6ms 一次),而 setInterval 只是机械地按时间间隔触发,不感知渲染管线。结果就是:用 setInterval 做动画时,经常出现「跳帧」「卡顿」「画面撕裂」,尤其在页面负载高或设备性能波动时。
常见错误现象包括:
- 动画看起来“抖”或“一顿一顿”
- 控制台里看到大量
Uncaught RangeError: Maximum call stack size exceeded(递归调用没终止) -
performance.now()显示帧间隔忽大忽小(比如 12ms / 24ms / 30ms 交替)
关键不是“能不能跑”,而是“能不能稳定在 60FPS”。requestAnimationFrame 自动处理了节流、后台标签页暂停、系统节能降频适配等细节,setInterval 全得自己补。
正确启动和终止 requestAnimationFrame 动画循环
写一个可持续运行又可安全停止的动画循环,核心在于「递归调用 + 终止条件 + 引用清理」:
- 必须在回调函数内部再次调用
requestAnimationFrame,否则只执行一帧 - 用变量保存上一次的
requestAnimationFrame返回值(一个数字 ID),以便后续调用cancelAnimationFrame - 终止逻辑不能只靠
return,必须显式调用cancelAnimationFrame,否则回调仍在调度队列中排队
let animationId = null;
<p>function animate(timestamp) {
// 执行动画逻辑(例如更新 position、重绘 canvas)
update();
render();</p><p>// 下一帧继续 —— 这行不能少
animationId = requestAnimationFrame(animate);
}</p><p>// 启动
animationId = requestAnimationFrame(animate);</p><p>// 停止(比如用户离开页面、组件卸载)
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}</p>容易踩的坑:
- 把
requestAnimationFrame(animate)写在函数外(变成单次调用) - 在 React/Vue 等框架中,组件卸载后没清理
animationId,导致内存泄漏和报错 - 多个动画共用同一个
animationId变量,互相覆盖导致无法准确取消
如何用时间戳实现帧率无关的动画逻辑
requestAnimationFrame 回调会传入一个高精度时间戳(单位毫秒,来自 performance.now()),这是做平滑动画的关键——它让你知道「这一帧距离上一帧过了多久」,从而让位移、旋转等计算与实际耗时挂钩,而不是假设“每帧都是 16.6ms”。
比如要实现匀速移动:
❌ 错误写法(帧率依赖):position += 2; → 在掉帧时会变慢,在高刷屏上会过快
✅ 正确写法(时间驱动):position += speed * (timestamp - lastTimestamp) / 1000;
使用场景:
- 物理模拟(速度、加速度)
- 拖拽跟随、缓动过渡(ease-in-out)
- 音视频同步动画(需严格对齐时间线)
参数差异注意点:
-
timestamp不是Date.now(),它是单调递增的,不受系统时间调整影响 - 第一帧的
timestamp可能很大(如页面加载后几秒才开始动画),所以务必初始化lastTimestamp为首次调用的值 - 不要直接用
timestamp % 1000做周期判断,应基于差值累加
兼容性与性能边界要注意什么
requestAnimationFrame 在所有现代浏览器中都支持(IE10+,Chrome 24+,Firefox 23+),但仍有几个真实存在的边界问题:
- iOS Safari 旧版本(< 15.4)在页面后台时可能不会真正暂停
requestAnimationFrame,导致电量异常消耗 - 某些安卓 WebView(如微信内置)对
requestAnimationFrame的调度精度偏低,实测帧间隔偏差可达 ±5ms - 如果一帧内执行 JS 耗时 > 16ms(比如复杂布局计算、大量 DOM 操作),浏览器会丢帧,但
requestAnimationFrame仍会继续调度——你得自己检测并降级逻辑(例如跳过部分粒子更新)
性能建议:
- 把重计算(如路径生成、碰撞检测)移到 Web Worker,只在主线程做渲染
- 避免在
requestAnimationFrame回调里频繁读取offsetTop、getBoundingClientRect(),它们会强制同步布局 - 使用
will-change: transform提前提示浏览器该元素将动画,触发图层提升
动画是否真能达到 60FPS,不取决于你用了哪个 API,而取决于每一帧的总耗时是否压在 16ms 内。这个数字很苛刻,也最容易被忽略。

















