requestAnimationFrame 和 requestIdleCallback 有什么区别?
简化版
requestAnimationFrame 会在下一次重绘前执行,适合动画和读写布局;requestIdleCallback 会在浏览器空闲时执行,适合低优先级后台任务。rAF 追求赶上下一帧,rIC 追求不影响关键渲染;不能用 rIC 做必须及时完成的任务。
详细版
requestAnimationFrame 的节奏和屏幕刷新相关,60Hz 下每帧约 16.7ms。它比 setInterval 更适合动画,因为浏览器可以把回调安排在重绘前,并在页面不可见时降低频率。
requestIdleCallback 适合拆分非紧急任务,例如预处理数据、上报日志、预加载低优先级内容。它的回调会收到 deadline,可通过 timeRemaining() 判断本次空闲还剩多少时间。
requestAnimationFrame(() => {
box.style.transform = 'translateX(100px)'
})
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
run(tasks.shift())
}
})
面试回答要强调:rAF 不是立即执行,rIC 也不保证一定很快执行。
完整版教学
一、浏览器一帧里要做很多事
页面不是 JS 一改 DOM 就马上显示到屏幕。浏览器通常要经历任务执行、微任务、样式计算、布局、绘制、合成等步骤,最后才产生一帧。
JS task -> microtasks -> requestAnimationFrame
-> style -> layout -> paint -> composite -> screen
在 60Hz 屏幕上,一帧预算约 1000 / 60 = 16.7ms。如果 JS、样式和布局总共超过这个预算,就可能掉帧。rAF 和 rIC 都是为了让开发者更好地配合浏览器调度。
面试时先讲帧预算,再讲 API 位置,会比直接背“rAF 动画、rIC 空闲”更有说服力。
二、requestAnimationFrame 适合下一帧前更新动画
requestAnimationFrame 告诉浏览器:我希望在下一次重绘前执行这个回调。它适合动画、滚动联动、需要和渲染节奏对齐的 DOM 更新。
let x = 0
function animate() {
x += 2
box.style.transform = `translateX(${x}px)`
if (x < 200) requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
rAF 是一次性的,如果要连续动画,回调里要再次调用。它的回调参数通常是时间戳,可以用时间差计算动画进度,避免不同刷新率下速度不一致。
let start
function step(ts) {
if (!start) start = ts
const progress = Math.min((ts - start) / 1000, 1)
box.style.transform = `translateX(${progress * 300}px)`
if (progress < 1) requestAnimationFrame(step)
}
三、为什么 rAF 比 setInterval 更适合动画
setInterval(fn, 16) 看起来接近 60fps,但它不理解浏览器渲染节奏。回调可能在一帧中间执行,也可能因为主线程忙而堆积或延后。
| 方案 | 是否对齐重绘 | 页面后台行为 | 适合场景 |
|---|---|---|---|
setInterval | 不保证 | 可能被节流 | 普通定时逻辑 |
setTimeout | 不保证 | 可能被节流 | 延迟任务 |
requestAnimationFrame | 对齐重绘前 | 页面不可见时通常降频或暂停 | 动画和渲染更新 |
数字例子:如果 setInterval 每 16ms 执行一次,但某次 JS 长任务耗时 50ms,后续回调会延迟,动画节奏就不稳定。rAF 则让浏览器按实际绘制节奏给你下一次机会。
记忆钩子:动画要跟屏幕刷新跳舞,rAF 是浏览器给你的节拍器。
四、requestIdleCallback 适合空闲时间做低优先级任务
requestIdleCallback 会在浏览器空闲时执行回调。它适合不紧急、可切片、可延后的任务,例如日志上报、缓存预热、低优先级数据处理。
const tasks = new Array(1000).fill(0).map((_, i) => i)
function work(deadline) {
while (deadline.timeRemaining() > 0 && tasks.length) {
expensiveWork(tasks.shift())
}
if (tasks.length) requestIdleCallback(work)
}
requestIdleCallback(work)
如果一次处理 1000 条数据需要 200ms,直接同步执行会造成明显卡顿。把它切成每次空闲处理一点,就能降低对交互和渲染的影响。
但 rIC 不适合关键任务。浏览器很忙时,空闲回调可能迟迟不执行;部分环境支持情况和调度策略也要考虑。
五、rIC 的 timeout 是兜底,不是实时保证
requestIdleCallback 可以设置 timeout,表示如果长时间没有空闲,也希望尽量执行。但这不是严格实时调度。
requestIdleCallback(processQueue, { timeout: 2000 })
如果任务必须在 500ms 内完成,例如支付状态确认、表单校验、路由跳转前鉴权,就不应该依赖 rIC。它更适合“晚一点也可以”的任务。
关键任务:用户输入响应、页面跳转、接口状态 -> 不放 rIC
空闲任务:日志、预计算、低优先级缓存 -> 可放 rIC
面试追问“rIC 能不能做埋点上报”,回答要分情况:普通曝光日志可以,退出前关键日志要结合 sendBeacon、visibilitychange 等兜底。
六、读写 DOM 要避免布局抖动
rAF 经常和 DOM 读写优化一起考。频繁交替读取布局和写入样式,会迫使浏览器提前计算布局,造成 layout thrashing。
// 不好:读写交替
items.forEach(item => {
const h = item.offsetHeight
item.style.height = h + 10 + 'px'
})
// 更好:先读后写
const heights = items.map(item => item.offsetHeight)
requestAnimationFrame(() => {
items.forEach((item, i) => {
item.style.height = heights[i] + 10 + 'px'
})
})
如果有 100 个节点,读写交替可能触发多次强制布局;先批量读,再在 rAF 中批量写,可以让浏览器更容易合并计算。
这也是 rAF 的工程价值:不只是做动画,还能把视觉更新放到合适时机。
七、常见误区与追问
- 误区:requestAnimationFrame 会立即执行。 它通常在下一次重绘前执行,不是同步调用。
- 误区:rAF 每 16ms 必然执行一次。 刷新率、页面可见性、主线程负载都会影响回调频率。
- 误区:requestIdleCallback 适合所有异步任务。 它只适合低优先级任务,关键任务不能依赖空闲时机。
- 追问:rAF 为什么比 setInterval 适合动画? rAF 与浏览器重绘节奏对齐,并能在后台页降频,减少无效工作。
- 追问:rIC 的 timeRemaining 有什么用? 用来判断本次空闲还剩多少预算,避免一次处理太久。
- 追问:如何避免布局抖动? 批量读、批量写,把视觉写入尽量放到 rAF 中。
八、加强记忆
rAF 和 rIC 可以记成“赶帧”和“捡空”:rAF 赶在下一帧前更新视觉,rIC 捡浏览器空闲做低优先级活。动画、滚动、视觉更新选 rAF;日志、预处理、缓存预热选 rIC;关键任务不要交给空闲调度。