跳到主要内容

requestAnimationFrame:为什么比 setTimeout 更适合动画?回调到底什么时候执行?

面试速答(30 秒版 TL;DR)

  • requestAnimationFrame(简称 rAF)是浏览器提供的 按下一帧渲染节奏调度回调 的 API,最适合做视觉更新。
  • 它的回调通常会在 下一次重绘前 执行,浏览器会尽量让 JS 更新和屏幕刷新对齐,避免无意义抖动。
  • 相比 setTimeout(fn, 16)rAF 的优势是:更贴近渲染时机、后台标签页会降频/暂停、更节能、动画更平滑
  • rAF 不是“固定 16.67ms 执行一次”;屏幕如果是 120Hz,回调节奏也可能接近 8.33ms。

心智模型:它不是“定时器”,而是“渲染前回调”

一句话理解:

  • setTimeout 关心的是“过多久再把任务丢回事件循环”
  • requestAnimationFrame 关心的是“浏览器准备绘制下一帧时,再给你一次机会更新画面”

所以视觉动画优先 rAF,不是因为它“更快”,而是因为它时机更对


最小例子:基于时间差驱动动画

let start = 0;

function step(timestamp) {
if (!start) start = timestamp;

const elapsed = timestamp - start;
const x = Math.min(elapsed / 5, 300);

box.style.transform = `translateX(${x}px)`;

if (x < 300) {
requestAnimationFrame(step);
}
}

requestAnimationFrame(step);

要点:

  • 回调参数 timestamp 是浏览器给你的高精度时间戳
  • 不要写“每次加 1px”,要基于 时间差 算位移,才能适配不同刷新率

rAFsetTimeout 的核心区别

维度requestAnimationFramesetTimeout
设计目标渲染前调度通用延时任务
执行时机下一帧绘制前时间到后进入任务队列
是否贴合屏幕刷新
后台标签页通常降频或暂停也会被节流,但语义不是为动画设计
适合场景动画、视觉同步更新轮询、延时、非视觉任务

面试常用一句话:

  • 动画不是“多久执行一次”,而是“下一帧该显示什么”

为什么动画更平滑

如果你用 setTimeout(fn, 16) 做动画,会遇到几个问题:

  • 16ms 只是理想值,真实执行会受主线程忙闲影响
  • 即使这次回调执行了,也不代表马上就会渲染
  • 更新和渲染不同步,容易出现掉帧、抖动、资源浪费

rAF 的优势就在于:

  • 浏览器能把样式计算、布局、绘制前的 JS 更新整合到一帧里
  • 背景页降频,避免无意义计算
  • 屏幕刷新率不同,也能更自然地对齐

timestamp 为什么重要

很多人写动画时犯的错是“每一帧固定加固定像素”:

let x = 0;

function step() {
x += 2;
box.style.transform = `translateX(${x}px)`;
requestAnimationFrame(step);
}

这在 60Hz 和 120Hz 设备上的速度会不同。

正确思路:

  • 真实经过的时间 计算位移,而不是按“执行了多少次回调”

取消动画:cancelAnimationFrame

let id = 0;

function loop() {
id = requestAnimationFrame(loop);
}

id = requestAnimationFrame(loop);
cancelAnimationFrame(id);

高频追问:rAF 在事件循环的哪个位置?

面试可这样回答:

  • 它不是普通宏任务,也不是 Promise 微任务
  • 它属于浏览器渲染管线相关的调度点
  • 一轮事件循环里,浏览器会在合适时机执行 rAF 回调,然后做样式计算、布局、绘制

不要求你背规范原文,但要说清:它和渲染绑定,而不是单纯的任务队列延迟执行


和 DOM 读写的配合方式

一个实战经验:

  • 读布局信息:getBoundingClientRect 等,尽量集中读
  • 写样式:transform / opacity 等,尽量集中写
  • 把“视觉更新”放进 rAF

这样做能减少布局抖动(layout thrashing)。


典型题 & 标准答法

Q1:为什么 requestAnimationFramesetTimeout(..., 16) 更适合动画?

标准答法:

  • rAF 会在浏览器下一帧绘制前执行,和渲染节奏对齐
  • setTimeout 只能保证“最早不早于某个时间”,不能保证什么时候渲染
  • 所以 rAF 更平滑、更省电、后台页面也更合理

Q2:requestAnimationFrame 一定是 16.67ms 一次吗?

不是。

  • 它取决于显示器刷新率和当前页面实际调度情况
  • 60Hz 大约 16.67ms
  • 120Hz 大约 8.33ms
  • 掉帧时也可能更久

Q3:用 rAF 做动画时,为什么建议使用 transform

因为:

  • transform / opacity 通常更容易走合成线程
  • 相比改 top / left,更不容易触发布局和重绘

易错点/坑

  • rAF 当成“定时器”使用,而不是“渲染前钩子”。
  • 不用 timestamp,导致高刷设备速度异常。
  • 动画结束后忘记停止递归调度。
  • rAF 里做过重计算,仍然会掉帧。

速记要点(可背诵)

  • rAF = 下一帧绘制前执行的回调。
  • 动画优先 rAF,不是因为更快,而是因为更贴近渲染。
  • timestamp 做时间差驱动,不要按“帧次数”驱动。
  • 停止动画用 cancelAnimationFrame