Case 10/13

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

In the wild

Where this rule bites real code

The progress bar that jumps from 0 to 100

Symptom
You update a progress element inside a loop over await-based work. The bar never shows intermediate values — it stays at 0 and then snaps to done.
Why
awaits resume as microtasks, and microtasks all run before the rendering step. If the loop never yields to a task, the browser gets no chance to paint between updates.
Fix
Yield a real task inside the loop — await new Promise(r => setTimeout(r, 0)) — or batch UI updates with requestAnimationFrame so each frame paints the latest value.

Reading layout at the wrong moment

Symptom
You set a class and immediately read offsetHeight to animate from the new size — but you measure the OLD layout, or you trigger a forced synchronous reflow that janks the frame.
Why
Style and layout are computed in the rendering step, after your code finishes. Reading layout mid-task forces the browser to do that work early, synchronously, every time you ask.
Fix
Write styles in one place, then read measurements inside a requestAnimationFrame callback (or double-rAF for post-paint) so reads happen after the browser has computed layout once.

Environment

Browser

CI verified

Node.js v22.22.3

requestAnimationFrame stubbed as a 16ms timer; ordering preserved

Last reviewed

29 July 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →