Learn the concept
Event Loop & Runtime
requestIdleCallback schedules low-priority work to run during idle periods when the browser's main thread has no other tasks. Unlike requestAnimationFrame (which runs before every repaint) or setTimeout (which has a minimum delay), it runs only when the browser is truly idle.
The browser's main thread handles JavaScript execution, layout, painting, and user input. When the main thread is busy, the UI becomes unresponsive. JavaScript provides several scheduling APIs that let you control when code runs relative to rendering and other work.
requestIdleCallback(callback, { timeout }) schedules a callback to run during idle periods — gaps between frames when the browser has no other work to do. The callback receives an IdleDeadline object with:
timeRemaining() — milliseconds left in the current idle period (typically 0-50ms)didTimeout — true if the callback was forced to run because the timeout option expiredrequestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.shift());
}
if (tasks.length > 0) {
requestIdleCallback(processRemainingTasks);
}
}, { timeout: 2000 }); // Force run within 2 secondsUse cases: analytics reporting, prefetching resources, non-critical DOM updates, indexing content for search.
| API | When it runs | Use case |
|-----|-------------|----------|
| queueMicrotask() | After current task, before rendering | Promise-like async, state synchronization |
| requestAnimationFrame() | Before next repaint (~16ms at 60fps) | Animations, visual updates |
| setTimeout(fn, 0) | Next macrotask (min ~4ms delay) | Yielding to event loop, breaking long tasks |
| requestIdleCallback() | During idle periods | Non-urgent background work |
| scheduler.postTask() | Priority-based scheduling | Fine-grained priority control (experimental) |
Runs once before every repaint. The browser calls your callback ~60 times per second (or matching the display refresh rate). Use it for smooth animations and DOM measurements:
function animate() {
element.style.transform = `translateX(${x++}px)`;
if (x < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Key difference from setTimeout: rAF is synchronized with the display refresh rate and pauses when the tab is hidden (saving resources).
The Scheduler API provides priority-based task scheduling:
user-blocking — highest priority (input handling, critical rendering)user-visible — default (visible updates)background — lowest priority (analytics, prefetching)Yields to the main thread while maintaining task priority, allowing the browser to handle pending user input before continuing your work. This is the modern replacement for the setTimeout(0) yielding pattern.
Tasks longer than 50ms are considered "long tasks" and block the main thread. Break them into smaller chunks:
async function processLargeArray(items) {
const CHUNK_SIZE = 100;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(processItem);
// Yield to main thread between chunks
await new Promise((resolve) => setTimeout(resolve, 0));
}
}requestIdleCallback is not available in Safari (as of 2024) — use a polyfill or fallback to setTimeoutrequestIdleCallback — the idle period can be invalidated by layout changes. Use it for computation, then apply DOM changes in a requestAnimationFramerequestIdleCallback but uses its own implementation for cross-browser consistency and more control over scheduling priorities// Queue of non-urgent tasks
const taskQueue = [];
function enqueueTask(task) {
taskQueue.push(task);
scheduleWork();
}
function scheduleWork() {
requestIdleCallback(processTasks, { timeout: 5000 });
}
function processTasks(deadline) {
// Process tasks while we have idle time
while (
taskQueue.length > 0 &&
(deadline.timeRemaining() > 0 || deadline.didTimeout)
) {
const task = taskQueue.shift();
task();
}
// If tasks remain, schedule for next idle period
if (taskQueue.length > 0) {
scheduleWork();
}
}
// Usage
enqueueTask(() => sendAnalytics({ page: '/home' }));
enqueueTask(() => prefetchRoute('/dashboard'));
enqueueTask(() => indexContentForSearch());Defer sending analytics events to idle periods so tracking code never blocks user interactions or visual updates.
Use idle time to prefetch resources for likely next navigations, improving perceived performance without impacting current page responsiveness.
Build a task scheduler that accepts tasks with priority levels (urgent, normal, idle) and dispatches them using requestAnimationFrame, setTimeout, and requestIdleCallback respectively.