requestAnimationFrame 是实现 JavaScript 动画最合理的选择,它比 setTimeout 或 setInterval 更精准、更省电、更贴合浏览器渲染节奏,能自动对齐刷新率、后台暂停、避免延迟累积,并需递归调用、正确使用时间戳实现帧率无关动画。

requestAnimationFrame 是实现 JavaScript 动画最合理的选择,它比 setTimeout 或 setInterval 更精准、更省电、更贴合浏览器渲染节奏。
为什么 requestAnimationFrame 比 setTimeout 更适合动画
浏览器每秒刷新屏幕约 60 次(即 60Hz),理想动画帧率应与之对齐。但 setTimeout(fn, 16) 只是“尽量”每 16ms 执行一次,实际执行时间受 JS 主线程阻塞、任务队列延迟等影响,容易掉帧或卡顿。
requestAnimationFrame 则不同:它把回调函数交给浏览器统一调度,在下一次重绘前执行,天然对齐刷新率,且在页面不可见(如切换标签页)时自动暂停,不浪费 CPU 和电量。
- 不会因主线程繁忙而累积延迟(
setTimeout会) - 自动节流:后台标签页中回调被暂停,恢复可见时继续
- 与 CSS 动画、WebGL 渲染等共享同一帧调度机制
requestAnimationFrame 的基本用法和常见错误
它不是循环执行的函数,而是一次性注册;要实现连续动画,必须在回调里递归调用自己。
立即学习“Java免费学习笔记(深入)”;
function animate() {
// 更新元素位置/样式
element.style.transform = `translateX(${x}px)`;
// 关键:主动再次请求下一帧
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
常见错误包括:
- 忘记递归调用
requestAnimationFrame→ 动画只执行一帧 - 在事件监听器中直接调用(如
click里写requestAnimationFrame(draw))却不取消前一次 → 多个动画同时运行,互相干扰 - 误以为它能传入毫秒参数 → 它只接受一个函数,没有第二个时间参数(时间戳由浏览器通过参数传入)
如何正确传递时间戳并做帧率无关的动画
requestAnimationFrame 回调默认接收一个高精度时间戳(单位毫秒,从页面加载开始),可用于计算真实经过时间,避免因帧率波动导致动画快慢不一。
let lastTime = 0;
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime; // 单位:毫秒
lastTime = timestamp;
// 例如:让物体以 100px/s 匀速移动
x += (100 / 1000) * deltaTime;
element.style.transform = translateX(${x}px);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
关键点:
- 用
timestamp而非Date.now(),前者更精确、无时钟漂移 - 不要假设每帧都是 16ms,必须用差值计算位移/旋转量
- 若需暂停/恢复,保存
lastTime和当前状态,而非依赖帧数计数
与 CSS transition / animation 的协作边界在哪
纯样式变化(如颜色、透明度、简单位移)优先用 CSS transition 或 @keyframes,因为它们可由 GPU 加速,且不触发重排(reflow)。
JS + requestAnimationFrame 的典型适用场景是:
- 需要逐帧逻辑判断(如碰撞检测、滚动视差、手写轨迹)
- 动画参数动态变化(如鼠标跟随、物理弹簧效果)
- 组合多个 DOM 元素的协同运动(CSS 难以表达复杂依赖)
- 需要精确控制启动/暂停/倒放/跳转到某帧
混合使用时注意:避免在 requestAnimationFrame 中频繁读取 offsetTop、getBoundingClientRect() 等触发同步布局计算,否则极易掉帧——应先批量读、再批量写。










