两个特殊任务:渲染前的 rAF,空闲时的 rIC
requestAnimationFrame 和 requestIdleCallback 既不是普通宏任务、也不是微任务,它们被固定在事件循环的特定时机上:rAF 在浏览器绘制 (paint) 之前执行——是做动画的正确工具;rIC
在绘制之后、主线程空闲时执行——适合处理不紧急的工作。一个在绘制前,一个在绘制后的空闲期。
1 · 一帧里发生了什么
浏览器大约每 16.7ms (60fps) 渲染一帧。把这一帧拆开,各类任务的位置是固定的:
所以 在 rAF 回调里修改样式,改动会赶上紧接着的这次绘制,动画平滑无撕裂;而 setTimeout 修改样式可能错过本帧、要等下一帧,看起来卡顿。rIC 在绘制之后,只有当这一帧还有富余时间才执行,且随时可能被推迟到后面的帧。
2 · rAF 做动画:跟着刷新率走
同一个方块,分别用 requestAnimationFrame 和 setInterval(16ms) 驱动。看 FPS 与顺滑度的差别:
rAF 的回调自带一个时间戳参数 (),它等于本帧的渲染时刻。用「位移 = 速度 × 实际经过的时间」而不是「每帧固定 +2px」,动画在 60Hz / 120Hz / 卡顿时都速度一致——这叫 delta-time。后台标签中 rAF 暂停,正好避免在用户看不见时无谓地消耗电量。
3 · rAF / rIC 在顺序里的位置
一次性排下四种,看真实执行顺序(rIC 不支持时用降级):
4 · rIC:用「本帧剩余时间」切分大任务
requestIdleCallback(cb) 给回调一个 deadline 对象,deadline.timeRemaining() 表示本次空闲还剩多少毫秒。典型用法:在 timeRemaining() > 0 时处理一部分,时间用完就
return 并再排一次,把大任务切分到各帧的空隙中,不阻塞交互。
rIC 不可靠,不要用它处理关键工作。主线程持续繁忙时它永远不会执行(除非提供 { timeout },超时后强制执行,此时 didTimeout = true)。它也并非所有浏览器都支持(Safari 长期未实现),生产环境常配降级方案。关键的后台调度,现在更推荐
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 时,要自己去重为一次。rAF 长期占用主线程时,Chrome
还会限流输入事件以缓解争用。