requestIdleCallback
Run low-priority work in idle periods — deadline.timeRemaining, timeout option, polyfill with rAF, and what not to put idle.
- javascript
- requestidlecallback
The main thread is busy with input, scripts, and rendering. requestIdleCallback schedules work for moments the browser believes it has spare time — analytics, prefetch warmup, non-critical indexing — without competing as hard as a naked setTimeout(0) loop.
const id = requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
const task = tasks.shift();
task();
}
if (tasks.length) {
requestIdleCallback(/* same */ , { timeout: 2000 });
}
}, { timeout: 2000 });
cancelIdleCallback(id);
deadline
| API | Meaning |
|---|---|
timeRemaining() |
ms estimate left in this idle period (can be 0) |
didTimeout |
true if invoked because timeout fired, not true idle |
function pump(deadline) {
do {
workOne();
} while (tasksLeft() && deadline.timeRemaining() > 1);
if (tasksLeft()) {
requestIdleCallback(pump, { timeout: 1000 });
}
}
Keep each unit of work small. One giant task inside idle still freezes the page.
timeout option
Without timeout, idle work may be delayed a long time on busy pages. With timeout: 2000, the callback runs by then even if the browser never felt “idle” — then didTimeout is true and timeRemaining() may be 0; still yield cooperatively.
Support and polyfill shape
Safari lagged for years; check current Baseline. Fallback:
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
Not perfect (doesn’t know real frame budget) but good enough for non-critical queues.
What belongs idle vs not
| Idle OK | Not idle |
|---|---|
| Beacon analytics | Input response |
| Hydration of below-fold widgets | Animations (use rAF) |
| Building search indexes of local data | Anything with hard latency SLAs |
| Prefetch of likely routes | Security-sensitive immediate work |
React’s older scheduler ideas and scheduler package overlap this space; framework integrations may already chunk rendering — don’t double-schedule naively.
Interaction with microtasks
Idle callbacks are tasks in the event loop sense (macrotask family), not microtasks. Don’t expect them between promise chains mid-task.
Interview answer (out loud)
“requestIdleCallback runs a callback when the browser has free time, with a deadline so I can chunk work via timeRemaining. The timeout option guarantees a latest run. I use it for low-priority non-visual work, not animations or input. I polyfill carefully where unsupported.”
Chunking algorithm sketch
function runIdle(tasks, { timeout = 1000 } = {}) {
function next(deadline) {
while (tasks.length && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {
const t = tasks.shift();
t();
if (deadline.didTimeout) break; // do one and reschedule when forced
}
if (tasks.length) requestIdleCallback(next, { timeout });
}
requestIdleCallback(next, { timeout });
}
Tune batching so forced-timeout runs don’t monopolize the main thread.
Analytics beacons
Idle is a good time to flush queued analytics, but use visibilitychange / pagehide + navigator.sendBeacon for last-chance delivery when the tab closes — idle may never come.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.