requestAnimationFrame 比 setTimeout 更适合动画,因其自动对齐屏幕刷新率、避免掉帧卡顿、后台暂停省电、支持浏览器统一优化;setTimeout 则易受主线程阻塞影响,导致帧率不稳和时间漂移。
因为 requestAnimationFrame 是浏览器专门为动画设计的调度机制,它会自动对齐屏幕刷新率(通常是 60Hz),而 setTimeout 完全依赖开发者手动控制间隔,容易导致掉帧、卡顿或过度绘制。
常见错误现象:用 setTimeout(fn, 16) 模拟 60fps 动画,但实际执行时间受 JS 主线程阻塞、任务队列延迟影响,帧率可能骤降到 30fps 甚至更低;同时多个 setTimeout 动画叠加时,还可能出现时间漂移——比如本该在第 5 帧结束的动画,拖到第 7 帧才停。
requestAnimationFrame 会在下一次重绘前执行回调,保证每次更新都“赶得上”渲染流水线requestAnimationFrame 自动暂停,不浪费 CPU;setTimeout 仍会持续触发requestAnimationFrame 做统一节流和优化(如降频到 30fps 保续航),setTimeout 无法参与这种协调核心是递归调用自身,并在每次回调中计算当前状态、更新样式、决定是否继续。
function animateElement(el, fromX, toX, duration = 300) {
const startTime = performance.now();
const startLeft = fromX;
const distance = toX - fromX;
function step(timestamp) {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // 防止超调
const currentX = startLeft + distance * progress;
el.style.transform = `translateX(${currentX}px)`;
if (progress < 1) {
requestAnimationFrame(step);
}}
requestAnimationFrame(step);
}
// 使用示例
const box = document.getElementById('my-box');
animateElement(box, 0, 200, 400);
注意点:
performance.now() 计算真实耗时,不能靠帧数 × 16ms 推算(因帧率不恒定)progress 来终止递归,否则动画停不下来
transform 而非 left/top,避免触发重排(layout)最典型的问题是「重复启动」和「未清理」:用户快速点击多次动画按钮,导致多个 requestAnimationFrame 链并发运行,互相干扰样式值。
正确做法是保存当前动画帧 ID,并在新动画开始前取消旧的:
let animationId = null;function startMoving(el, targetX) { cancelAnimationFrame(animationId); // 关键:先清旧任务
const startX = parseFloat(getComputedStyle(el).transform.split(',')[4]) || 0; const startTime = performance.now();
function step(timestamp) { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / 300, 1); const currentX = startX + (targetX - startX) * progress;
el.style.transform = `translateX(${currentX}px)`; if (progress < 1) { animationId = requestAnimationFrame(step); } else { animationId = null; // 清空 ID,便于下次判断 }}
animationId = requestAnimationFrame(step); }
animationId 变量,不要共用全局 IDanimationId 和状态存在实例属性里step 回调里直接修改 DOM 属性以外的东西(比如发请求、改全局变量),否则可能破坏帧一致性不是所有“动起来”的需求都适合 requestAnimationFrame。它本质是“每帧都要跑一次”的机制,开销比预期高。
transition 或 @keyframes 更轻量setInterval 或 setTimeout 更合适,强行用 requestAnimationFrame 反而增加主线程负担
AudioContext.currentTime 或 performance.now() 做校准,单靠 requestAnimationFrame 不够准真正需要它的场景很明确:连续、高频、需与屏幕刷新强同步的视觉变化——比如滚动视差、粒子系统、手绘轨迹、Canvas 游戏主循环。