Prefer reading? The full explanation
How Rendering Fits In
requestAnimationFrame vs microtasks · Browser
Before this
How Promises Run — the microtask checkpoint.
After this you can explain
Where painting sits in the loop, and how JavaScript makes the browser drop frames.
console.log('1: script');
requestAnimationFrame(() => console.log('4: rAF, just before paint'));
Promise.resolve().then(() => console.log('3: microtask'));
console.log('2: script end'); Output: 1: script → 2: script end → 3: microtask → 4: rAF, just before paint
Why does the microtask print before the rAF callback?
The output is 1 → 2 → 3 → 4. Both the promise callback and the requestAnimationFrame callback are deferred, but they are deferred to different points in the loop.
The microtask checkpoint runs at the end of the current task, as soon as the call stack is empty. The rendering step comes after that. So the promise callback prints 3 while the browser has not started rendering yet, and the rAF callback prints 4 once it has.
What a loop iteration really looks like
Run one task. Drain the entire microtask queue. Then, if it is time for a frame, run the rendering step: execute rAF callbacks, recalculate style, run layout, paint. Then go back and pick the next task.
This is why requestAnimationFrame is the right place to change the DOM for an animation. Your callback runs after all pending JavaScript has settled and immediately before the browser measures and paints, so the change lands in the frame about to be shown rather than the one after it.
How JavaScript drops frames
At 60fps the browser has roughly 16ms per frame, and it can only render between tasks. A task that runs for 50ms means the rendering step is simply skipped for three frames, and the user sees the page freeze.
Microtasks are the subtler version of the same problem. The queue is drained until empty, including microtasks queued during the drain, and the rendering step is on the other side of that drain. A promise chain that keeps queueing more work can therefore prevent painting indefinitely without any single long function ever appearing in a profile.
Nesting and visibility
A requestAnimationFrame call inside a rAF callback schedules the next frame, not another pass at the current one. The experiment below shows the two callbacks landing in separate frames roughly 16ms apart, which is exactly the pattern an animation loop relies on.
One practical difference from setInterval: when a tab is hidden the browser stops requesting frames, so rAF callbacks pause entirely rather than piling up. An interval-driven animation keeps firing in the background and burns battery for pixels nobody sees.
Reading about it is one thing. Watching it run is another.
↑ Run it step by step