requestAnimationFrame 比 setTimeout 更适合动画,因其与屏幕刷新率同步、自动暂停于后台、需递归调用、接收高精度时间戳、避免强制同步布局。

requestAnimationFrame 为什么比 setTimeout 更适合动画
浏览器在每次重绘前会自动触发 requestAnimationFrame,它和屏幕刷新率(通常是 60Hz)同步,能避免丢帧、卡顿或“撕裂”。而 setTimeout 是基于时间间隔硬调度的,容易因 JS 执行延迟、任务队列堆积导致回调跳帧或累积延迟。
- 即使设为
setTimeout(callback, 16),也不能保证每 16ms 执行一次;requestAnimationFrame则由浏览器决定最合适的时机 - 页面后台运行时,
requestAnimationFrame自动暂停,省资源;setTimeout仍持续触发 - 不支持页面缩放/高 DPI 自适应的动画(比如用
px硬写位移)照样卡,光换 API 没用
基础循环结构:别漏掉递归调用
requestAnimationFrame 只触发一次,要实现连续动画,必须在回调里再次调用自己——这是最容易忘的点。
function animate() {
// 更新状态:位置、透明度、旋转角度等
element.style.transform = `translateX(${x}px)`;
<p>// ✅ 关键:主动再请求下一帧
requestAnimationFrame(animate);
}
requestAnimationFrame(animate); // 启动</p>- 忘了在函数体内调用
requestAnimationFrame(animate),动画只执行一帧就停 - 不要用
setInterval(animate, 16)混搭,会和 RAF 冲突,造成双倍绘制或错乱 - 如果需要条件停止(如到达目标位置),在更新逻辑后加
if (!shouldContinue) return;,不要直接 return 而不调用下一次 RAF
如何正确传入时间戳做匀速动画
requestAnimationFrame 回调默认带一个高精度时间戳参数(单位毫秒,从页面加载开始),应优先使用它计算增量,而不是依赖 Date.now() 或自增计数器。
let startTime = null;
function animate(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
<p>// 例如:2 秒内从 0 移动到 400px
const x = Math.min(400, (elapsed / 2000) * 400);</p><p>element.style.transform = <code>translateX(${x}px)</code>;
requestAnimationFrame(animate);
}</p>- 直接用
timestamp,不是performance.now()——两者值接近,但 RAF 时间戳更稳定,且与帧节奏对齐 - 不要用
++frameCount来算进度,帧率波动时会导致速度忽快忽慢 - 若需缓动效果,把线性计算
x = ...换成easing(elapsed / duration),但依然基于timestamp
性能陷阱:DOM 读写和强制同步布局
动画中频繁读取 offsetLeft、getBoundingClientRect() 或写入 style.left 会触发强制同步布局(Layout Thrashing),严重拖慢帧率。
立即学习“前端免费学习笔记(深入)”;
- ✅ 只写不读:用
transform和opacity动画,它们走合成层,不触发布局 - ❌ 避免混用:比如先读
element.offsetWidth,再改style.width,浏览器必须立刻计算布局并返回值 - 如需基于尺寸做动画(如展开高度),先用
getComputedStyle一次性读完所有需要的值,再批量写入
真正卡顿往往不出现在 requestAnimationFrame 调用本身,而在于回调里干了不该干的事。测一帧耗时,用 performance.mark() + performance.measure() 比看控制台日志更准。



















