· By Wataru Ito · Front End · 7 min read
requestAnimationFrame (rAF) and requestIdleCallback (rIC)
A practical guide to JavaScript's render and idle phases, with real-world examples and code walkthroughs.

In my post about the JavaScript event loop, we explored how the browser schedules work. There are two more parts worth looking at:
- Rendering updates, where you can schedule a callback using requestAnimationFrame.
- Idle periods, where you can schedule a callback using requestIdleCallback.
The browser doesn’t go through both of these on every turn of the event loop. It decides when to update the screen and whether there is spare time for other work.
requestAnimationFrame
requestAnimationFrame, aka rAF, schedules a callback before the browser’s next repaint. Use it for animation and visual updates that you want to coordinate with rendering.
Unlike queueMicrotask, rAF waits for a rendering opportunity. It doesn’t have a fixed place right after your microtasks. The callback runs as part of the browser’s rendering update, before it paints the result to the screen.
Each request schedules one callback. To keep an animation going, call requestAnimationFrame again from your callback. In most browsers, rAF pauses in background tabs and hidden iframes.
requestIdleCallback
requestIdleCallback, aka rIC, schedules a callback for an idle period. Use it for small tasks that can wait, like preparing logs to send to a server.
If the browser stays busy, it might keep postponing your callback. In a hidden tab, the browser may reduce how often it offers idle time. You can set a timeout so the work doesn’t have to wait for an idle period, but this is not a guarantee of an exact execution time.
Like rAF, each request schedules one callback. Both APIs run your callbacks on the main thread in these examples. Using rIC doesn’t move work to a background thread.
Example for Both
I used AI to help write this example, then reviewed and edited it.
console.log("Start the script");
// Schedule a timer callback as a later task.
setTimeout(() => {
console.log("setTimeout");
}, 0);
queueMicrotask(() => {
console.log("queueMicrotask");
queueMicrotask(() => {
console.log("Nested queueMicrotask");
});
});
requestAnimationFrame(() => {
console.log("requestAnimationFrame");
});
if ("requestIdleCallback" in window) {
requestIdleCallback(
(deadline) => {
console.log("requestIdleCallback");
console.log(`Time remaining: ${deadline.timeRemaining().toFixed(2)}ms`);
console.log(`Called because of timeout: ${deadline.didTimeout}`);
},
{ timeout: 20000 }
);
} else {
console.log("requestIdleCallback is not supported in this browser");
}In this example, the first three logs are Start the script, queueMicrotask, and Nested queueMicrotask. The order of the timer, animation frame, and idle callbacks depends on the browser’s rendering schedule and workload. For example, setTimeout can run before rAF.
rIC is not available in all browsers, so check for support before calling it. This example skips it if it’s unavailable. For work you still need to do, you can use a timer as a fallback, as shown below. A timer doesn’t provide idle time or an idle deadline.
The requestAnimationFrame callback and timestamp
p5.js uses rAF under the hood for its drawing loop. A few things to keep in mind:
- With a loop that requests another frame each time, callbacks tend to follow the display’s refresh rate. At 60Hz, the interval is about 16.7ms; at 144Hz, it can be about 6.9ms. Neither rate is guaranteed. Heavy work or browser throttling can reduce it.
- Power-saving modes can reduce the frequency too. Some browsers have used 30fps instead of 60fps in low-power mode, but there is no rule that it must drop by half.
- A simplified rendering sequence is
rAF → Recalculate Style → Layout → Paint. This is an overview, not a complete list of browser steps. Reading layout information after changing the DOM can also force style or layout work inside your callback. - The callback receives a
timestamp, a DOMHighResTimeStamp in milliseconds relative to the page’s time origin, which is usually the start of navigation. It is not the time since your script started. For a window, it matchesdocument.timeline.currentTime, and callbacks in the same frame receive the same timestamp. - rAF returns an ID that you can pass to cancelAnimationFrame to cancel a pending callback. This is useful when stopping an animation or cleaning up a component.
This example counts for ten seconds from the first callback, leaving at least 50ms between counts:
let count = 0;
let startTimestamp;
let lastCountTimestamp;
function logCount(time) {
if (startTimestamp === undefined) {
startTimestamp = time;
lastCountTimestamp = time;
}
if (time - startTimestamp >= 10000) {
return;
}
if (time - lastCountTimestamp >= 50) {
console.log(count++);
lastCountTimestamp = time;
}
requestAnimationFrame(logCount);
}
requestAnimationFrame(logCount);The count happens on the first available frame after at least 50ms has passed, so this is not an exact 50ms timer. If a frame takes longer, this example skips missed counts instead of trying to catch up. If the tab goes into the background, the elapsed time still includes that gap; the loop stops on the next callback once ten seconds have passed.
The requestIdleCallback callback and options
The browser can have spare time between frames, or while you’re reading a page with nothing changing on screen. rIC lets you use some of that time for work that can wait. Think of it like doing small chores between growing potatoes on Mars.
Setting a timeout
Pass an options object as the second argument to set timeout in milliseconds. If that time passes before the callback runs, the browser queues a task to run it, even without idle time. It still has to wait for the browser to run that task; it cannot interrupt JavaScript that’s already running. The timeout doesn’t guarantee that the work will start or finish by that time.
Without a timeout, there is no guarantee of when your callback will run. Set one for work that needs to happen even if the browser doesn’t have spare time.
Checking the deadline
The callback receives a deadline object with two useful members:
deadline.timeRemaining()estimates how many milliseconds are left in the current idle period.deadline.didTimeoutistrueif the browser called your callback because the timeout expired. In that case,timeRemaining()returns0.
Like checking your oxygen supply on Mars, check the remaining time before taking on more work. The browser doesn’t stop your callback when the time runs out. Keep each piece of work small, and return control to the browser before doing the rest.
This example processes a queue of small tasks. On a timeout, it runs one task so the queue makes progress. Without rIC support, it uses a timer to run one task at a time:
const tasks = [
() => console.log("Process item A"),
() => console.log("Process item B"),
() => console.log("Process item C"),
];
let nextTask = 0;
const supportsIdleCallback = "requestIdleCallback" in window;
function runOneTask() {
const task = tasks[nextTask++];
task();
}
function scheduleWork() {
if (nextTask >= tasks.length) return;
if (supportsIdleCallback) {
requestIdleCallback(handleIdle, { timeout: 1000 });
} else {
setTimeout(handleTimer, 0);
}
}
function handleIdle(deadline) {
if (deadline.didTimeout) {
// Make progress, but keep the work in this callback small.
runOneTask();
} else {
while (nextTask < tasks.length && deadline.timeRemaining() > 1) {
runOneTask();
}
}
scheduleWork();
}
function handleTimer() {
runOneTask();
scheduleWork();
}
scheduleWork();The 1ms check leaves a small margin; it doesn’t guarantee that the next task will fit. Replace the logs with short pieces of work. One long task can still block input and rendering, even inside an idle callback. For visual updates, use rAF to coordinate your DOM changes with rendering.
Cancelling a pending callback
cancelIdleCallback cancels a callback that hasn’t started yet. It can’t stop one that is already running. This small example cancels a request before the browser gets a chance to call it:
if ("requestIdleCallback" in window) {
const pendingId = requestIdleCallback(() => {
console.log("This won't run because we cancel the request below");
});
cancelIdleCallback(pendingId);
}In an app, you might save the ID and cancel the pending request when the user leaves the page or the work is no longer needed. To stop work inside a running callback, return from it. To continue later, schedule another callback for the remaining work.
For more detail, see MDN’s requestAnimationFrame and requestIdleCallback documentation, the HTML rendering steps, and the requestIdleCallback specification.
I’ve updated this article since I first published it to correct technical errors and clarify the examples. I asked AI to help with that review and revision.
