skip to content

Rendering Pipeline

You will learn the path from bytes to pixels: parsing, style and layout, paint, compositing, and the frame loop that drives it. Interviewers ask because 'why is this janky?' is unanswerable without a mental model of which stage your change invalidates.

on this pageshow

explore

questions

30

Why is requestAnimationFrame preferred over setInterval(draw, 16) for driving a browser animation?

level: juniorimportance: must knowfreq 78%

answer

  1. schedule with the screen, not a clock
  2. once per frame, just before paint
  3. 16 ms and 16.7 ms drift apart
  4. refresh rate is not always 60 Hz
  5. hidden pages stop producing frames

basics

~20 s

requestAnimationFrame runs a callback once per frame, just before paint and in step with the display's refresh rate. A 16 ms interval drifts against that rhythm, duplicating or skipping frames, and it keeps firing in hidden tabs.

solid answer

~50 s

`requestAnimationFrame(cb)` asks the browser to call `cb` the next time it produces a frame, right before style, layout and paint run for that frame. That means the callback fires in step with the display: about every 16.7 ms on a 60 Hz screen, about every 8.3 ms on a 120 Hz one. `setInterval(draw, 16)` has no relationship to that rhythm — the delay is only a minimum, measured against the timer's own clock, so it slowly slides against the refresh interval and you get two draws inside one frame or none at all, which the user sees as stutter. It can also draw more often than the screen updates, burning CPU on pixels nobody sees. And when the page is hidden the browser simply stops producing frames, so a rAF loop idles automatically, while a timer keeps waking the page up.

code

javascript · 12 lines
javascript
const box = document.createElement('div');
box.style.cssText = 'position:absolute;top:40px;left:0;width:40px;height:40px;background:teal';
document.body.append(box);

let x = 0;
function draw() {
  x = (x + 2) % 300;
  box.style.transform = 'translateX(' + x + 'px)';
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

go deeper

for a junior

Be able to say that rAF runs your callback once per frame, right before the browser paints, and that a timer is not tied to the screen at all. Remember that the loop keeps going only because the callback asks for the next frame.

for a middle

Explain the drift concretely: 16 ms against a 16.7 ms refresh interval beats, so draws get doubled or dropped, and extra draws between paints are wasted work. Mention that the refresh rate is hardware-dependent and can change at runtime.

for a senior

Show that you reason about where the callback sits in the frame and what else has to fit in the same budget. Point out that rAF stops on a hidden page while timers keep firing, and that motion must be computed from elapsed time rather than a per-frame constant.

for a principal

Own the decision of which scheduling primitive a shared codebase uses for which class of work — visual updates on rAF, wall-clock work on timers, deferrable work on an idle scheduler — and be able to justify why mixing them causes both jank and wasted battery.

## What "one frame" means A browser does not repaint after every line of JavaScript. It produces *frames*: at a cadence set by the display, it stops accepting new work for that turn, recalculates style, lays out boxes, paints them, and hands the result to the compositor to be shown on screen. On a typical 60 Hz display that happens roughly every 16.7 ms; on a 120 Hz display roughly every 8.3 ms. Animating means changing something exactly once per frame, so what you want is a scheduling primitive that fires once per frame — no more, no less. ## What requestAnimationFrame actually does `requestAnimationFrame(callback)` adds `callback` to the document's list of animation frame callbacks and returns a positive integer handle. The next time the browser decides to produce a frame, it runs every callback on that list, passing each one the frame's start timestamp, and then continues into style recalculation, layout, paint and compositing for that same frame. The registration lasts for exactly one frame, so a continuous animation re-registers from inside its own callback: ```js function draw(timestamp) { // update state, write to the DOM requestAnimationFrame(draw); // book the next frame } requestAnimationFrame(draw); ``` Two properties follow from where the callback sits. First, it is invoked in step with the display rather than with a clock of your choosing. Second, it runs at the last moment where a DOM write still lands in the frame the user is about to see — the write is picked up by the style and layout passes that immediately follow. ## Why a 16 ms timer cannot line up `setInterval(draw, 16)` promises only "not sooner than 16 ms". The delay is measured from the timer's own bookkeeping, and it has no knowledge of the display's refresh signal. Sixteen and 16.7 are different numbers, so the two rhythms beat against each other: periodically the timer fires twice within one frame interval (the first draw is overwritten and never seen) and periodically it fires zero times (the same pixels are shown twice). The arithmetic in your animation is perfectly smooth and the motion still looks like it hitches. On top of that, timer callbacks are ordinary tasks queued behind everything else the page is doing. Under load they bunch up, so you may run several draws between two paints. Every draw but the last is wasted work, and the wasted work is what pushed the frame late in the first place. ## Refresh rate is not a constant Hardcoding 16 also bakes in an assumption about the hardware. High-refresh phones and monitors run at 90, 120 or 144 Hz, external displays and power-saving modes change the rate at runtime, and variable-refresh displays change it continuously. A rAF loop follows whatever the current rate is; a fixed interval either starves the display or over-produces. This is also why an animation should compute movement from elapsed time rather than adding a fixed number of pixels each callback — otherwise it literally runs twice as fast on a 120 Hz screen. ## Hidden pages When a tab is backgrounded or the page is otherwise not being rendered, the browser stops producing frames for it, and animation frame callbacks stop being invoked. A self-rescheduling rAF loop therefore parks itself with no extra code. Timers behave differently: browsers throttle them heavily in background tabs, but they do keep firing, so a `setInterval` animation goes on computing and mutating the DOM for a page nobody is looking at. ## What requestAnimationFrame does not promise It is not a promise of 60 fps, and it is not a budget enforcer. If the callback plus the style, layout and paint work that follows it cannot finish before the next display deadline, the frame is simply late and the user sees a lower rate. rAF also gives you no isolation: it runs on the main thread, in the same turn as everything else, so a long task elsewhere on the page delays it just as surely. ## When a timer is still the right tool Use a timer for anything that is not visual: polling, retry backoff, debouncing, session timeouts, a clock that ticks once a second. Those want wall-clock spacing, not display cadence, and they should keep working when the page is hidden. Reach for `requestAnimationFrame` only when the point of the callback is to change what is on screen for the next frame.

  • Does requestAnimationFrame guarantee your animation runs at 60 frames per second?
    No. It guarantees at most one callback per frame the browser actually produces. The rate comes from the display — 60, 90, 120 Hz — and drops whenever the callback plus the style, layout and paint work behind it cannot finish before the next deadline. It is a scheduling hook, not a performance guarantee.
  • Would setInterval at exactly the display's refresh interval fix the problem?
    No, for two reasons. You cannot read a reliable refresh interval to pass in, and it changes at runtime when the user moves the window to another monitor or the device drops into a power-saving mode. More fundamentally, the timer is still not phase-locked to the display's signal, so even a perfect period slides relative to the frames.
  • Is requestAnimationFrame the right tool for a background job that is not visual?
    No. It only runs when frames are being produced, so it stops entirely on a hidden page and its cadence is dictated by hardware you do not control. Non-visual work belongs on a timer, or on an idle-time or worker-based scheduler, depending on how urgent it is.

saying these in an interview costs you the question

  • Says requestAnimationFrame always runs at exactly 60 fps
  • Treats setTimeout(fn, 16) as equivalent to rAF
  • Thinks rAF guarantees the callback finishes within 16.7 ms
  • Uses rAF to schedule non-visual background work
  • Assumes every display refreshes at 60 Hz

context

open as a page

In the browser rendering pipeline, how do the CSS declarations `display: none` and `visibility: hidden` differ in whether the element gets a box, whether it occupies space, and what work the browser redoes when you switch each one back on?

level: juniorimportance: must knowfreq 72%

basics

~20 s

display: none keeps the element out of the render tree entirely: no box, no space, nothing painted. visibility: hidden still generates a box that occupies its layout space and only skips painting, so revealing it costs a repaint rather than a relayout.

open as a page

An HTML document has <script src="app.js"></script> in its <head> with no other attributes. What does the HTML parser do when it reaches that tag, and why does moving the same tag to just before </body> change what the user sees?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A classic script with no attributes is parser-blocking: the parser stops at the tag, waits for app.js to download and execute, then resumes. Placed before </body> the whole document is already parsed, so the user sees content first.

open as a page

In a browser page, when does the document's DOMContentLoaded event fire, and how is that different from the window load event?

level: juniorimportance: must knowfreq 75%

basics

~20 s

DOMContentLoaded fires as soon as the HTML has been fully parsed into a DOM and deferred scripts have run. It does not wait for images, iframes or stylesheets. The window load event fires later, once every subresource has finished loading.

open as a page

This loop runs over 500 elements and is dramatically slower than measuring every element first and then writing every new width. What is the browser doing on each iteration, and why does the split version avoid it? for (const box of document.querySelectorAll('.box')) { const w = box.getBoundingClientRect().width; box.style.width = w + 10 + 'px'; }

level: middleimportance: must knowfreq 60%

basics

~20 s

Each write leaves layout invalidated, and the next iteration's getBoundingClientRect forces the browser to recompute it, so 500 elements cost roughly 500 synchronous layouts. Reading all the widths first and writing afterwards collapses that to a single layout.

open as a page

In the browser, reading `el.offsetWidth` looks like a plain property access, yet it can be one of the most expensive lines in a frame. What does the browser actually do when that read happens, and when is the same read almost free?

level: middleimportance: must knowfreq 65%

basics

~20 s

Geometry reads such as offsetWidth and getBoundingClientRect must return current values, so the browser first flushes any pending style recalculation and layout synchronously, inside your task. When nothing has been invalidated since the last layout, the same read is nearly free.

open as a page

Where in a browser's event-loop turn does a requestAnimationFrame callback run relative to the task that scheduled it and the paint that follows?

level: middleimportance: must knowfreq 66%

basics

~20 s

An animation frame callback runs during the browser's rendering update: after the task that registered it has finished and its microtasks have drained, and before style recalculation, layout and paint for that frame. It is the last hook before pixels.

open as a page

A chart view starts a self-rescheduling requestAnimationFrame loop when it is shown. After the user moves between views several times the page grows steadily slower. What is going on and how do you fix it?

level: middleimportance: must knowfreq 57%

basics

~20 s

Each time the view is shown a new loop starts, and nothing ever stops the old ones, so every frame now runs several loops at once over detached elements. Store the handle requestAnimationFrame returns and call cancelAnimationFrame on teardown.

open as a page

In a browser's rendering pipeline, what does the layout stage (reflow) actually compute, and which kinds of changes to a page invalidate it?

level: middleimportance: must knowfreq 68%

basics

~20 s

Layout computes the used size and position of every box in the render tree. It is invalidated by DOM structure changes, geometric style changes, edited text, viewport resizes, and late-arriving fonts or images that change how much space content needs.

open as a page

In a browser, why does changing an element's `transform: translateX()` normally skip layout and paint work, while changing its `left` does not?

level: middleimportance: must knowfreq 72%

basics

~20 s

left is a layout property: changing it invalidates geometry, so the browser re-runs layout, repaints the affected content and recomposites. transform on a composited layer changes only the matrix the compositor uses to draw an already-rasterized texture, so layout and paint are skipped entirely.

open as a page

For an external classic script in HTML — <script src="a.js"></script> — what is the difference between adding the async attribute and adding the defer attribute, both in when the script executes and in what ordering guarantees you get across several such scripts?

level: middleimportance: must knowfreq 88%

basics

~10 s

Both download in parallel with parsing. An async script executes the moment its download finishes, interrupting the parser, in unpredictable order. A defer script executes only after parsing completes, in document order.

open as a page

Why is a `<link rel="stylesheet">` in a page's `<head>` described as render-blocking, and what is the browser doing while that file downloads?

level: middleimportance: must knowfreq 70%

basics

~20 s

The browser will not paint until it has a complete CSSOM, because painting with partial styles would show unstyled content and then repaint it. Meanwhile the HTML parser keeps running and building the DOM — only rendering is held, not parsing.

open as a page

In the DOM, why does reading `el.style.width` often return an empty string while `el.offsetWidth` returns a number, and which of the two can make the browser do layout work?

level: juniorimportance: should knowfreq 45%

basics

~20 s

el.style.width reads only the element's inline style attribute, so it is empty unless something set it inline. el.offsetWidth reports the element's actual rendered border-box width, so the browser may have to compute pending layout before it can answer.

open as a page

In a browser's rendering pipeline, what does the paint step actually produce, and how is that different from what the compositing step does?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Paint records the drawing commands for each box — backgrounds, borders, text, shadows — into a list, per layer. Compositing rasterizes those layers into GPU textures and assembles them, with each layer's transform and opacity applied, into the frame on screen.

open as a page

The callback passed to requestAnimationFrame receives a timestamp argument. What is that value, and why should an animation drive motion from it instead of advancing by a fixed amount each frame?

level: middleimportance: should knowfreq 52%

basics

~20 s

It is a high-resolution time in milliseconds, measured from the same origin as performance.now(), marking the start of the current frame. Using elapsed time between frames keeps motion at the same real-world speed on any refresh rate and across dropped frames.

open as a page

What is the render tree that a browser builds before layout, how does it relate to the DOM tree, and which parts of the document never appear in it?

level: middleimportance: should knowfreq 58%

basics

~20 s

The render tree pairs each rendered node with its computed style to produce the boxes the browser lays out. It is not a copy of the DOM: head content and display:none subtrees are missing, pseudo-elements add boxes with no node, and one element can produce several boxes.

open as a page

What makes a browser give an element its own compositing layer, and why can promoting one element cause neighbouring elements to get layers too?

level: middleimportance: should knowfreq 45%

basics

~20 s

Browsers promote content that must be drawn independently: 3D transforms, running transform or opacity animations, a will-change hint, video and WebGL canvas, and some fixed or scrolling content. Content that paints above a promoted layer may also need its own layer so the original paint order survives.

open as a page

In HTML, how does <script type="module" src="app.js"></script> behave with respect to the parser compared with a classic <script src="app.js"></script>, and what effect do the defer and async attributes have on a module script?

level: middleimportance: should knowfreq 50%

basics

~20 s

A module script is deferred by default: it never blocks the parser and runs after parsing, in document order. The defer attribute is therefore redundant, while async still works and makes it run as soon as its graph is ready — including on inline module scripts.

open as a page

What is the browser's preload scanner (also called the speculative or lookahead parser), and which resources on a page can it not discover?

level: middleimportance: should knowfreq 45%

basics

~20 s

The preload scanner is a secondary, lightweight parser that scans the raw HTML ahead of the main parser and starts downloading resources it finds in markup attributes. It cannot see anything that only exists after CSS or JavaScript runs.

open as a page

Walk through how a browser turns an incoming stream of HTML bytes into a DOM tree, and explain why it does not wait for the whole document to arrive first.

level: middleimportance: should knowfreq 52%

basics

~20 s

Bytes are decoded to characters, a tokenizer state machine turns those into start-tag, end-tag, text and comment tokens, and a tree constructor turns tokens into nodes and attaches them to the DOM. It runs on each chunk as it arrives so the page can render before the document ends.

open as a page

Several floating panels on a page each position themselves in a scroll listener by reading `getBoundingClientRect()` on their anchor element and then writing `style.transform` on the panel, and scrolling stutters badly. How would you restructure that with `requestAnimationFrame` so the page performs at most one layout per frame?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Coalesce the scroll events into one requestAnimationFrame callback per frame with a scheduled flag, and inside that callback read every anchor rectangle first and write every transform afterwards. That yields one layout per frame instead of one per panel per event.

open as a page

A canvas animation driven by requestAnimationFrame lurches far forward the instant the user switches back to the tab after a minute away. Why does it do that, and how do you keep it smooth?

level: seniorimportance: should knowfreq 42%

basics

~20 s

No frames are produced for a hidden page, so the loop is suspended. The first callback after the page becomes visible carries a timestamp about a minute past the stored one, and delta-time integration applies that whole gap in a single step. Clamp the delta.

open as a page

On a 60 Hz display, the work inside a requestAnimationFrame callback consistently takes about 25 ms. What frame rate does the user actually see, and why is it not roughly 40 fps?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Roughly 30 fps, not 40. The display only presents at its refresh boundaries, so a frame that misses one deadline waits for the next: effective rates fall to submultiples of the refresh rate — 60, then 30, then 20 — rather than degrading smoothly.

open as a page

Changing one element's height sometimes relayouts only that element's subtree and sometimes the whole document. What decides the scope of a reflow, and which page structures widen it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Scope depends on whether the change can escape the element. If the element's own size is fixed and its overflow cannot reach ancestors, the browser relayouts only its subtree; content-dependent sizing, auto-layout tables, in-flow siblings and a newly appearing scrollbar push the work document-wide.

open as a page

A browser tab's main thread is blocked by a long synchronous JavaScript task. A CSS `transform` animation keeps moving smoothly while a JavaScript-driven animation of the same element freezes. Why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The CSS animation was handed to the compositor thread, which owns the layer's texture and can tick the transform and draw frames without the main thread. The JavaScript animation writes styles from main-thread callbacks, so while that thread is blocked nothing updates.

open as a page

What does declaring `will-change: transform` in CSS ask the browser to do, and what goes wrong when it is applied to many elements or left declared permanently?

level: seniorimportance: should knowfreq 50%

basics

~20 s

will-change is a hint that a property is about to change, letting the browser prepare — usually by creating a compositing layer up front. Applied broadly or left on permanently it keeps those layers alive, consuming GPU memory and compositor bookkeeping, which slows the page instead of speeding it up.

open as a page

A script element is created with document.createElement('script'), given a src, and appended to the head at runtime. Does it block the HTML parser, and in what order do several scripts created this way execute?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A dynamically inserted script never blocks the HTML parser and behaves as async by default: each one runs as soon as its own download finishes, so order is unpredictable. Setting script.async = false before appending restores insertion-order execution.

open as a page

A page loads one stylesheet from its head, and that file begins with `@import url("theme.css")`. Why does first paint arrive later than if both files had been linked from the HTML head?

level: seniorimportance: should knowfreq 36%

basics

~20 s

An @import is only discovered after the parent stylesheet has been downloaded and parsed, so the two requests run one after the other instead of in parallel. The preload scanner cannot see inside CSS, so nothing starts the second fetch early.

open as a page

What do the CSS `contain` property and `content-visibility: auto` promise the browser's layout engine, and what do you give up in exchange?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

CSS containment promises that an element's internals cannot affect the rest of the page, so layout can stop at its boundary. content-visibility: auto goes further and skips off-screen subtrees entirely, at the price of guessed sizes, scroll-position accuracy and some side effects on positioning and clipping.

open as a page

Why is document.write() treated as unsafe on a modern web page — what happens when it runs after the document has finished parsing, and what does it do to loading when it is used to inject a <script src> tag?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Called during parsing it injects markup into the token stream, but called after the document has loaded it implicitly reopens the document and wipes everything already there. Injecting a script tag with it also forces a parser-blocking fetch on the critical path.

open as a page