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”,要基于 时间差 算位移,才能适配不同刷新率
rAF 和 setTimeout 的核心区别
| 维度 | requestAnimationFrame | setTimeout |
|---|---|---|
| 设计目标 | 渲染前调度 | 通用延时任务 |
| 执行时机 | 下一帧绘制前 | 时间到后进入任务队列 |
| 是否贴合屏幕刷新 | 是 | 否 |
| 后台标签页 | 通常降频或暂停 | 也会被节流,但语义不是为动画设计 |
| 适合场景 | 动画、视觉同步更新 | 轮询、延时、非视觉任务 |
面试常用一句话:
- 动画不是“多久执行一次”,而是“下一帧该显示什么”。
为什么动画更平滑
如果你用 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:为什么 requestAnimationFrame 比 setTimeout(..., 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。