两个特殊任务:渲染前的 rAF,空闲时的 rIC
requestAnimationFrame 和 requestIdleCallback 既不是普通 macrotask、也不是 microtask,它们被固定在事件循环的特定时机上:rAF 在浏览器绘制 (paint) 之前执行——是做动画的正确工具;rIC
在绘制之后、主线程空闲时执行——适合处理不紧急的工作。一个在绘制前,一个在绘制后的空闲期。
1 · 一帧里发生了什么
浏览器按显示器刷新率渲染,60Hz 下约每 16.7ms 一帧、120Hz 下约 8.3ms 一帧。把一帧拆开,各类任务的位置是固定的:
注 · 在 rAF 回调里修改样式,改动会赶上紧接着的这次绘制,动画平滑无撕裂;而 setTimeout 修改样式可能错过本帧、要等下一帧,看起来卡顿。rIC 在绘制之后,只有当这一帧还有富余时间才执行,且随时可能被推迟到后面的帧。
2 · rAF 做动画:跟着刷新率走
同一个方块,分别用 requestAnimationFrame 和 setInterval(16ms) 驱动。看 FPS 与顺滑度的差别:
注 · rAF 的回调自带一个时间戳参数 requestAnimationFrame(t => …),它等于本帧的起始时刻。用「位移 = 速度 × 实际经过的时间」而不是「每帧固定 +2px」,动画在 60Hz / 120Hz / 卡顿时都速度一致——这叫 delta-time。后台标签中 rAF
暂停,正好避免在用户看不见时无谓地消耗电量。
3 · rAF / rIC 在顺序里的位置
一次性排下四种,看真实执行顺序(rIC 不支持时用降级):
4 · rIC:用「本帧剩余时间」切分大任务
requestIdleCallback(cb) 给回调一个 deadline 对象,deadline.timeRemaining() 表示本次空闲还剩多少毫秒。典型用法:在 timeRemaining() > 0 时处理一部分,时间用完就
return 并再排一次,把大任务切分到各帧的空隙中,不阻塞交互。
警示 · rIC 不可靠,不宜用于关键工作。主线程持续繁忙时它永远不会执行(除非提供 { timeout },超时后强制执行,此时 didTimeout = true)。它也并非所有浏览器都支持:Chrome 47、Firefox 55 起支持,Safari
桌面端至今只在预览版且需开启开关,iOS Safari 未实现(核对于 2026-08,BCD 8.0.11),生产环境常配降级方案。关键的后台调度,现在更推荐 Scheduler.postTask。
5 · rAF 落在同帧还是下一帧
rAF 不是「永远下一帧才跑」这么简单,关键看回调在哪里排进去:在事件回调 (event handler) 里排的 rAF,跑在紧接着的同一帧;而在另一个 rAF 回调里排的 rAF,会被推到下一帧(IntersectionObserver /
ResizeObserver 的回调也算这个时机)。下面在一次 click 里连排 A、B,并在 A 内部再排 C,看三者各自拿到的 frame timestamp:
注 · 同一帧里所有 rAF 回调拿到的 timestamp 完全一样——它是本帧主线程的起始时刻,不是「各回调被调用的那一刻」。这正是做动画想要的:同帧的多个动画共用一个时间基准,天然同步。要子帧精度(比如量某段计算耗了多久)就自己 performance.now()。
警示 · rAF 不会自动限流,也不会分摊到多帧。同一帧排了 5 个各 100ms 的 rAF,浏览器会在这一帧里全部执行完(约 500ms),导致 jank。合并 (coalesce) 需要自行处理:多个模块各自 requestAnimationFrame 写 DOM 时,要自己去重为一次。