← 返回题目列表

requestAnimationFrame 和 requestIdleCallback 有什么区别?

高频 中等 第 12 / 30 题 更新于 2026/07/29
浏览器渲染requestAnimationFramerequestIdleCallback性能

简化版

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;关键任务不要交给空闲调度。