requestAnimationFrame

浏览器提供的动画调度 API,回调在下一次渲染前执行。与 setTimeout 不同,它与显示器刷新率同步(通常 60fps),是实现流畅动画的标准方式。

定义

requestAnimationFrame(rAF) 是浏览器提供的动画调度 API。传入的回调函数会在浏览器下一次重绘之前执行,确保动画更新与屏幕刷新率同步。

javascript
const id = requestAnimationFrame(callback);
cancelAnimationFrame(id); // 取消尚未执行的回调

requestAnimationFrame 返回一个整数 ID,可以传给 cancelAnimationFrame 来取消回调——两者成对使用,类似 setTimeout / clearTimeout 的关系。

在事件循环中的位置

根据 WHATWG HTML 规范的 event loop processing model,rAF 回调在渲染流水线中有独立的执行时机,处于微任务清空之后、浏览器实际渲染之前:

text
宏任务执行 → 微任务队列清空 → rAF 回调执行 → Style → Layout → Paint

rAF 回调既不属于宏任务队列,也不属于微任务队列——它是渲染步骤的一部分。这意味着在 rAF 回调中修改 DOM 或样式,会在同一帧内被渲染引擎处理,不会产生额外的重排。

与 setTimeout 的区别

维度requestAnimationFramesetTimeout(fn, 16)
同步刷新率自动适配显示器(60Hz/120Hz/144Hz)固定间隔,可能与刷新率偏移
页面隐藏时自动暂停,节省资源继续执行,浪费 CPU
执行时机渲染前,保证视觉一致性下一轮宏任务,可能错过渲染时机
动画流畅度稳定无撕裂可能掉帧或画面撕裂
时间精度回调接收高精度时间戳需要手动 Date.now()performance.now()

典型应用场景

1. 平滑动画

最基本的用途——用 rAF 驱动逐帧动画,取代 setInterval

javascript
function animate(timestamp) {
  const elapsed = timestamp - startTime;
  const progress = Math.min(elapsed / duration, 1);

  element.style.transform = `translateX(${progress * 300}px)`;

  if (progress < 1) {
    requestAnimationFrame(animate);
  }
}

const startTime = performance.now();
const duration = 1000;
requestAnimationFrame(animate);

rAF 回调接收一个 DOMHighResTimeStamp 参数(单位毫秒,精度到微秒),用它计算帧间隔可以实现与帧率无关的动画——无论 60Hz 还是 144Hz 屏幕,动画总时长一致。

2. 滚动/Resize 节流

用 rAF 替代传统的 throttle 函数处理高频事件:

javascript
let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateScrollPosition(window.scrollY);
      ticking = false;
    });
    ticking = true;
  }
});

每帧最多处理一次,自动与渲染同步。比 lodash.throttle(fn, 16) 更精准。

3. Canvas 游戏循环

Canvas 2D 或 WebGL 的渲染循环标准写法:

javascript
function gameLoop(timestamp) {
  update(timestamp);  // 更新游戏状态
  render();           // 重绘画面
  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

4. DOM 批量更新

当需要在一帧内收集多次状态变化并统一应用到 DOM:

javascript
let pending = false;
let updates = [];

function scheduleUpdate(change) {
  updates.push(change);
  if (!pending) {
    pending = true;
    requestAnimationFrame(() => {
      applyUpdates(updates);
      updates = [];
      pending = false;
    });
  }
}

React 的 setState 批处理和 Vue 的 nextTick 在部分场景下采用了类似的思路。

可视化:rAF 驱动的缓动小球

rAF 缓动动画

注意事项

  • 每次调用只注册一次回调:rAF 回调内再次调用 requestAnimationFrame 注册的回调在下一帧执行,不会在当前帧无限递归
  • 后台标签页行为:在后台标签页中 rAF 会被暂停或降频至约 1fps,节省系统资源
  • 必须成对使用:通过 cancelAnimationFrame(id) 取消回调,避免组件卸载后继续执行动画导致内存泄漏
  • 不要在 rAF 中做重计算:每帧只有约 16ms(60Hz),超时会导致掉帧。将耗时计算拆分或移到 Web Worker
javascript
// React 组件中的正确用法——cleanup 时取消 rAF
useEffect(() => {
  let id;
  function tick() {
    // 动画逻辑...
    id = requestAnimationFrame(tick);
  }
  id = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(id);
}, []);

引用本术语的文章