The requestIdleCallback() function
requestIdleCallback(callback, [options]) registers a callback that runs when the browser is not busy painting or handling events, that is, during idle time. It is a way to do background work without stealing time from rendering and without freezing the interface.
Theory
TL;DR
requestIdleCallback(fn)putsfninto the idle callback queue with the lowest priority.- The callback receives a
deadlineobject withtimeRemaining()anddidTimeout. - It is called in an idle window between frames, once rendering and microtasks have finished.
{ timeout: N }guarantees the call happens no later than N ms, even if no idle window appeared.- It returns a numeric ID and is cancelled with
cancelIdleCallback(id). - Not for critical work: on a loaded or hidden tab the call can be postponed for a long time.
Quick example
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
console.log('Doing a background task');
}
console.log('The browser is busy again, waiting for the next idle window');
});Here:
deadlineis a special object passed into the callback;deadline.timeRemaining()shows how many milliseconds are still free before the next frame;- you do work while there is time, and if you did not finish, the browser calls the callback again in the next idle window.
What idle time is and how the call works
The browser paints frames roughly 60 times per second, that is every 16.6 ms. If some frame took only, say, 10 ms, the browser is left with about 6 ms of free time, and that is exactly where your requestIdleCallback can land. This is how you run minor or background tasks that are not time critical without slowing the interface down.
Step by step:
- You call
requestIdleCallback(fn). - The browser adds
fnto the idle callbacks task queue. - Once the Event Loop, painting and microtasks are done and there is still time before the next frame, the browser calls your callback.
- If the tab is inactive or there is no free time, the callback can be postponed.
The arguments: deadline and timeout
requestIdleCallback(callback, { timeout: 2000 });callback(deadline)is a function that receives an object:deadline.timeRemaining(), how many milliseconds are left "until busy";deadline.didTimeout,trueif the timeout expired and the callback was invoked under duress.
options.timeoutis the maximum waiting time. If no free moment ever appears, the callback still runs after the given number of milliseconds.
Checking didTimeout lets you tell the two modes apart: during a real idle window you can safely work while timeRemaining() > 0, whereas on a forced call timeRemaining() is usually zero and you should do only the bare minimum.
Splitting heavy work into chunks
const tasks = Array.from({ length: 10000 }, (_, i) => i);
function processTasks(deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
// do a portion of the work
console.log('Task processed', task);
}
if (tasks.length > 0) {
requestIdleCallback(processTasks);
}
}
requestIdleCallback(processTasks);This approach lets you process thousands of items without lag: the UI stays responsive because the work happens in the pauses between frames. The key detail is re-registering the callback until the queue is empty, since a single call is almost never enough.
Comparison with setTimeout and requestAnimationFrame
| Criterion | setTimeout | requestAnimationFrame | requestIdleCallback |
|---|---|---|---|
| Goal | Run after N ms | Run before the frame is repainted | Run in the pause between frames |
| Priority | Medium | High (for animations) | Low (background tasks) |
| Depends on load | No | Yes, synchronised with the frame | Yes, called only when the browser is free |
| On an inactive tab | May be throttled | Is paused | May be delayed for a long time |
| Used for | Timers, delays | Animations, smooth updates | Light background computation, caching, prefetch logic |
Browser support and a fallback
requestIdleCallback is not available in every browser, Safari in particular does not have it, so it is better to keep a fallback:
const ric = window.requestIdleCallback || function (cb) {
return setTimeout(() => cb({ timeRemaining: () => 0, didTimeout: true }), 1);
};It must not be used for critical tasks, because it may not fire for a long time if the tab is under load.
Summary:
| Property | Value |
|---|---|
| What it does | Calls the callback when the browser has free time |
| Passes an object | deadline with timeRemaining() and didTimeout |
| When it runs | In idle windows between frames |
| Used for | Background tasks, caching, prefetching data |
| Cancelled with | cancelIdleCallback(id) |
| Returns | A numeric ID, like timers do |
Common mistakes
- Relying on it for critical logic. The callback may not run for minutes if the page is constantly busy. Set a
timeoutif you need a guarantee. - Ignoring
timeRemaining(). A loop without a remaining-time check eats the frame and brings back exactly the lag you were escaping. - Not re-registering the callback. A single idle window rarely fits all the work, so you have to call
requestIdleCallbackagain at the end if the queue is not empty. - Painting from it. DOM changes inside an idle callback happen after the frame and cause an extra reflow. Visual updates belong in
requestAnimationFrame. - Forgetting about missing support. Without a fallback the code simply does nothing in Safari, and the bug stays unnoticed for a long time.
- Not cancelling a scheduled callback. After a component disappears the call stays in the queue, so
cancelIdleCallback(id)is needed.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.