Under the Hood
Eventloop

Where rendering happens in the loop

Rendering is not something the browser squeezes in whenever it feels like it — it is a specific, throttled step wedged into the event loop right after a task and its full microtask drain, and that one placement is what requestAnimationFrame actually is.

Where rendering happens in the loop

Two lessons back, the loop looked like: run a task, drain the microtasks, maybe render, run the next task. That "maybe render" was doing a lot of quiet work, and it's time to open it up. Rendering — the whole chain of style, layout, paint, and composite that turns your DOM and CSS into pixels — is not sprinkled between tasks whenever the browser has a spare moment. It is one specific step in the loop's per-turn sequence, it runs in a fixed position relative to the task and its microtasks, and it is throttled to roughly the display's refresh rate rather than firing after every single task. Once you can see exactly where that step sits, requestAnimationFrame stops looking like a special kind of timer and becomes exactly what its name says: a callback that runs as part of rendering.

Rendering is a step, not a background process

Pick up the loop where the microtasks lesson left it: run one task to completion, then drain the microtask queue completely. Only after that drain finishes does the loop ask a question it did not have to ask before every task: is it time to produce a frame? If the answer is yes, the browser runs its rendering step — and only then does the loop go back and pick the next task. If the answer is no, rendering is skipped entirely and the loop goes straight to the next task, no frame produced, no style or layout work done.

That "is it time" check is the throttle. The browser doesn't try to render after every task — that could mean hundreds of render passes a second for a page dispatching lots of small tasks, almost all of them pointless because the screen itself only refreshes about sixty times a second. Instead the rendering step is paced to roughly match the display's refresh cadence — about once every 16.6ms — and only runs when there's actually a next refresh to prepare for and something that plausibly changed. Between two tasks, the browser may render. It does not always render.

This is why the microtask starvation hazard is so total. The rendering step comes strictly after the microtask checkpoint in the sequence, so if the checkpoint never finishes draining — a microtask endlessly requeuing itself — the loop never even reaches the "is it time to render" question. A burst of microtasks inside one checkpoint doesn't get a render squeezed into the middle of it, no matter how long that burst runs. Tasks, by contrast, do let rendering in between them, because each task ends its own turn cleanly and hands control back to the loop, which checks the render step before starting the next one.

requestAnimationFrame: a hook into this exact step

Look at where requestAnimationFrame callbacks sit in the diagram above: first, inside the rendering step, before style, layout, paint, or composite even run. That position is the entire contract. requestAnimationFrame(fn) doesn't schedule fn as a task and it doesn't schedule it as a microtask — it registers fn to run at that specific point, the moment the browser has decided it's time to render and is about to compute style. This is precisely why it's the correct place to make a visual change: whatever your rAF callback writes to the DOM gets picked up by the style/layout/paint/composite that runs immediately afterward, in the very same rendering step, so it shows up in the frame that's about to be produced rather than waiting for some later, unrelated pass.

Contrast that with the timer alternative:

// setTimeout: scheduled as an ordinary task. It has no idea where the
// render step is. It might land right before one, right after one, or
// in the middle of a run of tasks with no render in between at all.
setTimeout(() => {
  el.style.transform = `translateX(${x}px)`;
}, 16);

// requestAnimationFrame: scheduled to run inside the rendering step
// itself, immediately before style/layout/paint. The write below is
// guaranteed to be picked up by the paint that follows it.
requestAnimationFrame(() => {
  el.style.transform = `translateX(${x}px)`;
});

A setTimeout callback is just another task; it takes its place in the task queue like anything else and has no special relationship to the render step that follows it. It might run and then get a render, or it might run as one of several tasks the loop chews through before rendering happens at all. A requestAnimationFrame callback, by definition, only ever runs as part of a rendering step that's already been decided on — so a write inside it is never "wasted" on a turn where no frame gets produced.

This also folds the batching pattern into the same picture. When a handler stashes a value into a ref on every pointermove and only reads it back inside a single rAF callback, it's really doing this: keep the DOM untouched across however many tasks or microtasks fire before the next rendering step, then make exactly one write at the one moment guaranteed to land in the upcoming frame. One rendering step, one write — never more, because there's only one rendering step per frame to write into.

Skipping the render step entirely

Nothing forces the browser to render on a given turn. If it isn't yet time for the next refresh, or if nothing observable changed since the last render, the step is skipped outright — no style recalculation, no layout, no paint, no composite, and no rAF callbacks run either, since they're part of the step that got skipped. This is consistent with everything before it: rendering is throttled work slotted into specific turns of the loop, not a tax paid after every task.

When rendering escapes its slot: forced synchronous layout

There's one way the neat "rendering only happens in its one designated step" picture gets broken, and it's worth naming here even though it's a lesson of its own. Layout is normally lazy — the browser is happy to let style and DOM changes pile up and compute geometry once, in the rendering step, right before paint. But certain JavaScript reads — offsetHeight, getComputedStyle(), and similar geometry queries — demand an up-to-date answer the instant you ask for it. If you read one of those in the middle of a task, after having just written to the DOM, the browser can't wait for its scheduled rendering step to answer you; it computes layout right then, synchronously, in the middle of your task, out of its normal slot. Do that in a loop that alternates writes and reads and you get layout thrashing — dozens of forced layout passes crammed into a single task instead of the one lazy pass the rendering step would have done on its own. The fix in that lesson — batch all your reads before any of your writes — is really just "don't force layout out of the slot this lesson describes."

One full iteration of the loop

Put every piece from this module together and a single turn of the loop reads as one sentence: pick a task, run it to completion, drain the microtask queue completely, and then — if it's time — run any pending requestAnimationFrame callbacks, recalculate style, compute layout, paint, and composite, before going back to pick the next task. Every async feature covered so far is a statement about where in that sentence your code lands.

Where this goes next

Everything up to here has been about when code runs relative to the stack, the two queues, and the render step. The next lesson turns back to a mechanism that's been mentioned constantly but not yet explained from the inside: Promises and async/await takes the microtask machinery from lesson 4 and shows exactly how a Promise's state changes trigger .then() reactions, and how await desugars into precisely that — the last piece needed to read any async function as ordinary event-loop mechanics instead of magic.

Go deeper

  • MDN — requestAnimationFrame The precise contract for when the callback runs relative to the browser's paint cycle, matching the rendering-step position this lesson describes.
  • web.dev — Rendering performance The browser team's own breakdown of the style/layout/paint/composite pipeline this lesson slots into the event loop.
  • WHATWG HTML spec — Update the rendering The authoritative algorithm for the 'update the rendering' step, including the throttling and per-task placement this lesson walks through in prose.

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. In the loop's per-turn sequence, exactly where does the rendering step sit relative to a task and its microtask checkpoint?
  2. Why does the browser not simply render after every single task, and what roughly paces how often it does render?
  3. Explain why a burst of microtasks that never lets its checkpoint finish also prevents any rendering from happening, even though rendering isn't a microtask itself.
  4. Where exactly do requestAnimationFrame callbacks run relative to style, layout, and paint, and why does that position make rAF the correct hook for a visual update?
  5. Contrast a DOM write made inside a setTimeout callback with one made inside a requestAnimationFrame callback, in terms of which rendering step is guaranteed to pick it up.
  6. What causes the browser to compute layout synchronously in the middle of a task instead of waiting for its normal rendering step, and what everyday code pattern triggers it repeatedly?
  7. State the one full loop iteration in a single sentence, naming every step from picking a task through composite.