requestAnimationFrame 比 setTimeout 更适合动画,因其自动对齐屏幕刷新率、避免掉帧卡顿、后台暂停省电、支持浏览器统一优化;setTimeout 则易受主线程阻塞影响,导致帧率不稳和时间漂移。

requestAnimationFrame 为什么比 setTimeout 更适合做动画
因为 requestAnimationFrame 是浏览器专门为动画设计的调度机制,它会自动对齐屏幕刷新率(通常是 60Hz),而 setTimeout 完全依赖开发者手动控制间隔,容易导致掉帧、卡顿或过度绘制。
常见错误现象:用 setTimeout(fn, 16) 模拟 60fps 动画,但实际执行时间受 JS 主线程阻塞、任务队列延迟影响,帧率可能骤降到 30fps 甚至更低;同时多个 setTimeout 动画叠加时,还可能出现时间漂移——比如本该在第 5 帧结束的动画,拖到第 7 帧才停。
-
requestAnimationFrame会在下一次重绘前执行回调,保证每次更新都“赶得上”渲染流水线 - 页面切换到后台标签页时,
requestAnimationFrame自动暂停,不浪费 CPU;setTimeout仍会持续触发 - 浏览器可对
requestAnimationFrame做统一节流和优化(如降频到 30fps 保续航),setTimeout无法参与这种协调
如何用 requestAnimationFrame 实现一个基础位移动画
核心是递归调用自身,并在每次回调中计算当前状态、更新样式、决定是否继续。
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);
}}
立即学习“Java免费学习笔记(深入)”;
requestAnimationFrame(step);
}
// 使用示例
const box = document.getElementById('my-box');
animateElement(box, 0, 200, 400);
注意点:
- 必须用
performance.now()计算真实耗时,不能靠帧数 × 16ms 推算(因帧率不恒定) - 要显式判断
progress 来终止递归,否则动画停不下来 - 推荐用
transform而非left/top,避免触发重排(layout)
requestAnimationFrame 在循环动画中的常见陷阱
最典型的问题是「重复启动」和「未清理」:用户快速点击多次动画按钮,导致多个 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,便于下次判断 }}
立即学习“Java免费学习笔记(深入)”;
animationId = requestAnimationFrame(step); }
- 每个动画实例应独占一个
animationId变量,不要共用全局 ID - 如果动画逻辑复杂(如含 easing 函数、多属性联动),建议封装成类,把
animationId和状态存在实例属性里 - 不要在
step回调里直接修改 DOM 属性以外的东西(比如发请求、改全局变量),否则可能破坏帧一致性
什么时候其实不该用 requestAnimationFrame
不是所有“动起来”的需求都适合 requestAnimationFrame。它本质是“每帧都要跑一次”的机制,开销比预期高。
- 仅需一次视觉反馈(如按钮点击涟漪、toast 弹出)→ 用 CSS
transition或@keyframes更轻量 - 低频状态变化(如倒计时数字每秒更新一次)→
setInterval或setTimeout更合适,强行用requestAnimationFrame反而增加主线程负担 - 需要精确控制毫秒级时序(如音频可视化同步)→ 得结合
AudioContext.currentTime或performance.now()做校准,单靠requestAnimationFrame不够准
真正需要它的场景很明确:连续、高频、需与屏幕刷新强同步的视觉变化——比如滚动视差、粒子系统、手绘轨迹、Canvas 游戏主循环。











