requestIdleCallback
Squeeze Work Into the Gaps Between Frames
LinkedIn Hook
You have 16.67 milliseconds per frame at 60fps. The browser uses most of that for input, layout, and paint. If you steal a millisecond you shouldn't, you drop a frame and the user sees jank.
So where do you put the non-critical stuff — analytics beacons, prefetch hints, cache warming, idle DOM measurements?
Answer:
requestIdleCallback. The browser hands you adeadlineobject saying "I've got 3.2ms free before the next frame — do what you can." You loop whiledeadline.timeRemaining() > 1, drain a chunk of work, and reschedule the rest.In this lesson: basic rIC usage, a real task queue that processes work across multiple idle periods, a side-by-side comparison of
requestAnimationFramevsrequestIdleCallbackvssetTimeout, and a Safari polyfill since Safari still doesn't support it in 2026.If you've been stuffing non-urgent work into
setTimeout(fn, 0)— there's a better way.Read the full lesson -> [link]
#JavaScript #WebPerformance #requestIdleCallback #EventLoop #60fps #Frontend #InterviewPrep
What You'll Learn
- Where
requestIdleCallbackfits inside a 16.67ms browser frame and how thedeadlineobject works - How to build a task queue that drains non-critical work across multiple idle periods without dropping frames
- How
requestAnimationFrame,requestIdleCallback, andsetTimeoutdiffer — and how to polyfill rIC for Safari
The To-Do List Analogy
Think of requestIdleCallback like a to-do list you only work on when you're free. If your main job (rendering, handling user input) keeps you busy, the to-do list waits. But the moment you have a gap — between frames, after a burst of activity — you pick up where you left off.
How the Browser Frame Works
One frame at 60fps ~= 16.67ms
[ Input handling ] -> [ JavaScript ] -> [ Style ] -> [ Layout ] -> [ Paint ] -> [ Idle time ]
^
requestIdleCallback runs here
Basic Usage
// Schedule work for when the browser is idle
requestIdleCallback((deadline) => {
// deadline.timeRemaining() — ms left in this idle period
// deadline.didTimeout — true if the timeout was reached
while (deadline.timeRemaining() > 0) {
// Do a small chunk of non-critical work
doSmallTask();
}
});
// With timeout — guarantees execution within N ms even if busy
requestIdleCallback(myCallback, { timeout: 2000 });
// If browser hasn't been idle in 2 seconds,
// force-runs the callback (deadline.didTimeout === true)
Practical Example: Batch Non-Critical DOM Updates
// Scenario: Update analytics, prefetch resources, and log metrics
// None of these should block the user's interaction
const taskQueue = [];
function addIdleTask(task) {
taskQueue.push(task);
scheduleIdleWork();
}
let idleCallbackId = null;
function scheduleIdleWork() {
if (idleCallbackId) return; // already scheduled
idleCallbackId = requestIdleCallback((deadline) => {
idleCallbackId = null;
// Process tasks while we have idle time
while (taskQueue.length > 0 && deadline.timeRemaining() > 1) {
const task = taskQueue.shift();
task();
}
// If tasks remain, schedule another idle callback
if (taskQueue.length > 0) {
scheduleIdleWork();
}
});
}
// Queue non-critical work
addIdleTask(() => {
// Send analytics event
navigator.sendBeacon("/analytics", JSON.stringify({ page: "/home", time: Date.now() }));
});
addIdleTask(() => {
// Prefetch next likely page
const link = document.createElement("link");
link.rel = "prefetch";
link.href = "/dashboard.js";
document.head.appendChild(link);
});
addIdleTask(() => {
// Compute and cache layout measurements
const rect = document.getElementById("content").getBoundingClientRect();
sessionStorage.setItem("content-rect", JSON.stringify(rect));
});
Comparison: rAF vs rIC vs setTimeout
// requestAnimationFrame — for VISUAL work (animations, DOM updates the user sees)
requestAnimationFrame((timestamp) => {
// Runs before every paint (~60fps)
// Use for: animations, scroll effects, visual transitions
element.style.transform = `translateX(${position}px)`;
});
// requestIdleCallback — for NON-CRITICAL background work
requestIdleCallback((deadline) => {
// Runs when browser has spare time
// Use for: analytics, prefetch, caching, non-urgent computation
while (deadline.timeRemaining() > 0) {
processNextAnalyticsEvent();
}
});
// setTimeout — for DELAYED work (general scheduling)
setTimeout(() => {
// Runs after minimum delay (not guaranteed timing)
// Use for: debouncing, delayed UI, retry logic
retryFailedRequest();
}, 1000);
| Feature | requestAnimationFrame | requestIdleCallback | setTimeout |
|---|---|---|---|
| Purpose | Visual updates (animations) | Background non-critical work | General delayed execution |
| Timing | Before next paint (~16.7ms) | When browser is idle | After min delay (unreliable) |
| Frequency | ~60 times/sec (sync with display) | Varies (idle periods) | Once per call |
| Paused in background tab? | Yes (throttled) | Yes | Throttled to 1/sec |
| Receives | timestamp (DOMHighResTimeStamp) | IdleDeadline object | Nothing |
| Cancel | cancelAnimationFrame(id) | cancelIdleCallback(id) | clearTimeout(id) |
| Use when | Moving pixels | Logging, prefetch, cleanup | Delays, debounce, polling |
Polyfill Strategy
// requestIdleCallback is not supported in Safari (as of 2025)
// Simple polyfill using setTimeout
window.requestIdleCallback = window.requestIdleCallback || function(callback, options) {
const start = Date.now();
return setTimeout(() => {
callback({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start));
// 50ms is a safe estimate for "idle-like" time
}
});
}, 1);
};
window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
clearTimeout(id);
};
Common Mistakes
- Doing a single big blob of work inside the callback instead of chunking it — if your loop runs past
deadline.timeRemaining(), you push the next frame and cause jank. Always gate the loop withwhile (deadline.timeRemaining() > 1). - Using
requestIdleCallbackfor critical UI work — if the tab is busy, the callback may never fire (or only fire when you set atimeout). UserequestAnimationFramefor anything the user will see visually. - Forgetting that Safari still doesn't support
requestIdleCallbackin 2026 — ship a polyfill or detect and fall back tosetTimeout(fn, 1), otherwise those code paths are dead on iPhone.
Interview Questions
Q: What is requestIdleCallback and when would you use it?
requestIdleCallbackschedules a callback to run when the browser's main thread is idle — after rendering and event handling are done, before the next frame needs to start. Use it for non-critical work like analytics, prefetching resources, non-urgent DOM measurements, or processing queued background tasks. It receives anIdleDeadlineobject withtimeRemaining()to know how much time you have.
Q: What is the difference between requestAnimationFrame and requestIdleCallback?
requestAnimationFrameruns before each paint and is for visual work — animations, DOM updates users see, anything tied to rendering. It fires ~60 times per second in sync with the display refresh.requestIdleCallbackruns during idle periods between frames and is for background work — analytics, prefetch, caching. It fires only when the browser has spare time.
Q: How would you handle requestIdleCallback not being supported in Safari?
Use a polyfill that falls back to
setTimeoutwith a short delay (1-4ms). The polyfill creates a fakeIdleDeadlineobject with atimeRemaining()that estimates ~50ms of available time. This doesn't truly detect idle time but provides a compatible API so your code works everywhere. Libraries likeidle-callback-polyfillon npm offer battle-tested implementations.
Q: What is deadline.timeRemaining() and how do you use it?
deadline.timeRemaining()returns the number of milliseconds the browser expects to remain idle during this callback, with a maximum of 50ms. You use it as a loop guard so you only do as much work as fits:while (queue.length && deadline.timeRemaining() > 1) { queue.shift()() }. If work remains, callrequestIdleCallbackagain to continue in the next idle period.
Q: What does the timeout option on requestIdleCallback do?
requestIdleCallback(cb, { timeout: 2000 })tells the browser: run this callback within 2 seconds even if no idle time is available. When the timeout is reached, the callback runs anyway anddeadline.didTimeoutistrue(withtimeRemaining()typically 0). Use it for work that's low priority but still has an eventual deadline — e.g., flushing an analytics buffer before unload.
Quick Reference — Cheat Sheet
requestIdleCallback — WORK IN THE GAPS
API
const id = requestIdleCallback(cb, { timeout?: ms })
cancelIdleCallback(id)
INSIDE THE CALLBACK
cb(deadline):
deadline.timeRemaining() -> ms left (max ~50)
deadline.didTimeout -> forced by timeout
PATTERN: drain a queue
while (queue.length && deadline.timeRemaining() > 1) {
queue.shift()()
}
if (queue.length) requestIdleCallback(cb)
rAF vs rIC vs setTimeout
rAF -> before paint, ~60fps, for pixels
rIC -> idle periods, non-critical work
setTimeout -> min delay, debounce/retry
USE rIC FOR
analytics beacons, prefetch <link>, cache warm,
lazy measurements, background indexing
DON'T USE rIC FOR
user-visible animations (use rAF)
time-critical work (use microtasks / rAF)
SAFARI (2026) — still no native support
polyfill: setTimeout fallback with 50ms pseudo-deadline
Previous: Web Workers -> Parallel JavaScript Without Freezing the UI Next: Tree Shaking -> Ship Less Code With Dead Code Elimination
This is Lesson 13.4 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.