requestIdleCallback vs requestAnimationFrame
requestAnimationFrame schedules work right before the next repaint; requestIdleCallback runs only when the browser is otherwise idle. When to use each one.
requestAnimationFrame schedules a callback to run right before the browser’s next repaint, making it the right tool for anything visual; requestIdleCallback schedules a callback to run only when the browser has spare time between other work, making it the right tool for low-priority background tasks. Both exist to keep work off the main thread at the wrong moment — they just define “the wrong moment” differently.
requestAnimationFrame: synced to the repaint
Every frame the browser renders, it runs a sequence of steps: process input, run JavaScript, recalculate style, layout, paint. requestAnimationFrame (often shortened to rAF) schedules your callback to run at a specific point in that sequence — right before the browser is about to paint the next frame.
function animate(timestamp) {
element.style.transform = `translateX(${getOffset(timestamp)}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
This timing matters for two reasons. First, it’s synchronized to the display’s refresh rate (typically 60 times a second, sometimes higher), so animations driven by rAF are as smooth as the hardware allows — smoother than a setTimeout-based animation, which runs on a fixed timer that has no relationship to when the browser actually paints. Second, the browser automatically pauses rAF callbacks in a background or hidden tab, which setTimeout doesn’t do on its own — saving battery and CPU on work the user can’t even see.
Anything that changes what’s on screen frame by frame belongs here: CSS property animations driven from JavaScript, canvas drawing, scroll-linked effects, game loops. If a task doesn’t need frame-by-frame precision — a debounced input handler, say — reach for debounce or throttle instead.
requestIdleCallback: only when nothing else is waiting
requestIdleCallback (rIC) takes the opposite approach: instead of running at a fixed point in the frame cycle, it runs only during the gaps — the leftover time in a frame after rendering and other higher-priority work is done, or when the main thread has nothing scheduled at all.
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.pop());
}
}, { timeout: 2000 });
The callback receives a deadline object with timeRemaining(), telling you how much idle time is actually left in this slice — the expectation is that you do a chunk of work, check the deadline, and stop (yielding the rest to a later idle period) rather than running until you’re done regardless of how long it takes. The optional timeout guarantees the callback still fires eventually even if the browser is never idle, at the cost of potentially interrupting real work to force it through.
This fits work that’s useful but not urgent: pre-fetching data the user probably needs next, logging or analytics batching, warming a cache, or breaking a large computation into chunks so it doesn’t block anything the user is actively doing. It’s explicitly not for anything visual — because idle time is unpredictable, an animation driven by rIC would stutter unpredictably instead of running smoothly.
requestAnimationFrame vs requestIdleCallback
| requestAnimationFrame | requestIdleCallback | |
|---|---|---|
| Timing | Right before next repaint | Only during idle gaps |
| Frequency | Up to display refresh rate (~60Hz+) | Unpredictable, whenever idle |
| Guarantees timing? | Yes, tied to the render cycle | No, timeout only forces eventual execution |
| Paused when tab hidden | Yes | Continues, though idle periods shrink |
| Best for | Visual updates, animation, canvas | Background prefetching, logging, chunked work |
| Wrong tool for | Long-running background tasks | Anything the user needs to see change smoothly |
Browser support caveat
requestIdleCallback is not implemented in every browser engine — notably, Safari has never shipped it. Code that depends on it for correctness (rather than as a performance nicety) needs a fallback, commonly a setTimeout with a short delay as a rough approximation of idle time. requestAnimationFrame has universal support and doesn’t need one.
How they relate to the rest of the rendering pipeline
Both APIs are about when your code runs relative to the browser’s own work, which is the same concern behind the critical rendering path and Core Web Vitals: a long JavaScript task that blocks the main thread delays paint regardless of which scheduler you used to kick it off. Breaking up expensive work — using rIC’s timeRemaining() to chunk it, or moving it off the main thread entirely with a web worker — is what actually prevents jank; the scheduling API just decides which category of work gets priority.
The takeaway
Use requestAnimationFrame for anything the user sees change — it’s tied to the repaint cycle and runs as smoothly as the display allows. Use requestIdleCallback for background work that’s useful but not urgent, and design it to check timeRemaining() and yield rather than assuming it gets to run to completion. Neither replaces genuinely expensive work being moved off the main thread — they only control when on-thread work gets its turn.
Keep reading
Takina · · 6 min read How to Break Up Long Tasks in JavaScript
Long tasks block the main thread for 50ms or more and make pages feel frozen. How to split them with yielding, scheduler.yield(), postTask, and workers.
Takina · · 5 min read Microtasks vs Macrotasks in JavaScript, Explained
Microtasks (promise callbacks) run before the next macrotask (timers, events). How the two queues are ordered, and why it matters for your code.
Takina · · 5 min read OffscreenCanvas API Explained
OffscreenCanvas lets you render canvas graphics off the main thread, in a web worker, so heavy drawing work stops blocking scrolling, input, and animation.