PipelineGENERALENGINE-SPECIFICSIMPLIFIED

The Rendering Pipeline

DOM plus CSSOM become computed styles, then geometry, then paint commands, then a composited frame — and most updates re-run only part of that.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What does the browser actually do between a change to the page and a new frame on the screen?

The user intent

Someone drags a slider, opens a menu, or scrolls a long list. They expect the picture to keep up with their finger — updating in step with the movement, not a beat behind it.

The obvious build

Changing the DOM or the CSS makes the browser redraw the page. Redraws are expensive, so the goal is to do fewer of them.

Why it breaks

"Redraw" is not one operation. Changing color and changing width produce visually similar-sized updates and cost the browser entirely different amounts of work, because one of them re-runs geometry for a subtree and the other does not (The Cost of a Change).

How it breaks in a real browser
  • "Redraw" is not one operation. Changing color and changing width produce visually similar-sized updates and cost the browser entirely different amounts of work, because one of them re-runs geometry for a subtree and the other does not (The Cost of a Change).
  • You are usually not choosing how many redraws happen. The browser batches style and layout invalidations and resolves them once, at a rendering opportunity — a hundred style writes inside one task still produce one frame (The Rendering Opportunity).
  • Reading a geometric property back — offsetHeight, getBoundingClientRect() — forces the browser to run layout immediately, inside your task, before it has produced any pixels at all. The "redraw" you were counting has not happened yet, and now layout has run twice (Layout Thrashing).
  • A page that scrolls badly may be doing zero layout. The cost can be entirely paint on a shadow-heavy card, or entirely too many composited layers competing for GPU memory (Layer Explosion).
  • The budget is a deadline, not a rate. The display asks for a frame on a fixed cadence; work that does not finish in time is not a slower frame, it is a missing one, and the user sees a jump (The Frame Budget).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The parser turns bytes into a DOM tree and stylesheets into the CSSOM. Neither is pixels; both are object models the rest of the pipeline reads (The DOM Is Not Your HTML, The CSSOM).
  • Style computes, for every element that the engine has marked dirty, one final value per property — by matching selectors, resolving the cascade, and applying inheritance (Style Calculation).
  • Layout turns computed styles plus content into geometry: the size and position of every box whose measurements could have changed. This is where 50%, auto and min-content finally become numbers, because they depend on a containing block that only exists here (Intrinsic Sizing and the Automatic Minimum).
  • Between layout and paint the engine decides which content gets its own composited layer and builds the paint order — the stacking contexts, the clips, the transforms (Compositing Layers).
  • Paint does not put pixels on the screen. It records a list of drawing commands — fill this rounded rect, draw this glyph run, blur this shadow — per layer (Paint Commands).
  • Composite rasterises those commands into tiles, often on other threads or the GPU, and assembles the layers into the frame that is handed to the display (Cheap and Expensive Animation).
  • Each stage marks the next one dirty only if it has to. That is the whole game: a change that only alters colour never reaches layout, and a change the compositor can apply on its own never re-enters the main thread at all.

What this makes the browser do

And which of it is avoidable.

  • Recalculating style for the invalidated set, which may be one element or the whole document depending on which selectors mention what changed (Style Invalidation).
  • Running layout for dirty subtrees, propagating size changes up to ancestors whose size depends on their content and down to descendants whose size depends on their parent — unless a containment or size boundary stops it (CSS Containment).
  • Rebuilding the layer tree when promotion-triggering properties appear or disappear, allocating GPU memory per layer.
  • Recording paint commands for changed regions, then rasterising tiles at the current device pixel ratio — the same visual area costs more pixels on a high-density display.
  • Compositing, which is the one stage that can often proceed without the main thread — which is exactly why a frozen main thread can still scroll but cannot respond (What the Main Thread Owns).
  • Avoidable work, in rough order of how often it is wasted: recalculating style for elements nothing changed on, laying out subtrees whose geometry could not have moved, painting shadows and filters that redraw every frame, and rasterising layers that were promoted and then never animated.

Five stages, and what each one owns

The pipeline exists because the stages need different information. Style cannot be computed until the DOM and the CSSOM both exist. Geometry cannot be computed until style is known, because whether a box is flex or block decides what its size even means. Paint commands cannot be recorded until geometry is known, because you cannot draw a border without knowing where it is. And nothing can be shown until the layers are assembled.

The useful consequence is that the stages form a dependency chain in one direction only. Work at a later stage never dirties an earlier one. That is why "does this change geometry?" is the single highest-value question you can ask about any update: a yes means every stage after it runs too.

Inputs and stages
parseparseif geometry may changeif pixels may changeinvalidatesforces, if it reads geometryderived fromHTML bytesCSS bytesScript mutates DOM / styleDOM treeCSSOMStyle: computed valuesAccessibility treeLayout: geometryPaint: draw commandsRaster + compositeFrame on screen
UserLLMAgentToolDataDecisionHumanGuardrail
From a change to a frame
  1. 1
    DOM + CSSOM

    Parse markup into a tree of nodes and stylesheets into rules. Both are live object models; scripts mutate the first and can mutate the second.

    fails by A render-blocking stylesheet that has not arrived stalls everything downstream, no matter how small the HTML was.

  2. 2
    Style

    For each invalidated element, match selectors, resolve the cascade, apply inheritance, and produce one computed value per property.

    fails by Invalidating far more elements than changed — a class toggled high in the tree that many descendant selectors mention.

  3. 3
    Layout

    Resolve computed styles plus content into boxes with a size and a position. Percentages, auto and intrinsic sizes become numbers here.

    fails by Running early and repeatedly because code read geometry back between writes, or propagating up and down a subtree nothing bounded.

  4. 4
    Layerise + paint

    Decide which content gets its own composited layer, build the paint order, then record drawing commands per layer.

    fails by Large blurs, shadows and filters recorded every frame; or hundreds of promoted layers, each carrying GPU memory.

  5. 5
    Raster + composite

    Turn drawing commands into pixels in tiles, then assemble the layers into a frame for the display. Often off the main thread.

    fails by Missing the display's deadline, which drops the frame rather than delaying it — the user sees a jump, not a slow move.

The arrow only points forward. Nothing paint does can dirty layout, and nothing the compositor does can dirty style — which is exactly why changes that enter late are cheap.

Different updates enter at different stages

ENGINE-SPECIFICWhich elements get their own composited layer is an engine heuristic, not a spec rule: Blink, Gecko and WebKit promote on overlapping but different criteria and revise them between versions. The "composite only" row is therefore a description of a path that exists, not a guarantee that your element is on it — verify with layer tooling rather than assuming.

This is the entire point of the module, and it is the part the naive model gets wrong. The browser does not have one "update the page" routine. It has an entry point per stage, and a change is only as expensive as the earliest stage it dirties plus everything after it.

That is why two changes that look equally trivial in source can differ by an order of magnitude in cost, and why two changes that look very different can cost the same. The source does not tell you; the stage does. The next lessons make each row of this idea precise — how style decides what to recompute (Style Invalidation), and what a specific change costs (The Cost of a Change).

ChangeEarliest dirty stageWhat therefore runsWhy
Insert or remove an elementDOM, then styleStyle, layout, paint, compositeA new box has no computed style and no geometry, and its siblings may be positioned relative to it.
Change width, padding, font-sizeLayoutLayout, paint, compositeGeometry changed, so everything downstream of geometry is stale.
Change color, background-color, box-shadowPaintPaint, compositeThe boxes are in the same places; only the drawing commands differ.
Change transform or opacity on a composited layerCompositeComposite onlyThe layer's recorded pixels are unchanged; the compositor applies a new matrix or alpha when assembling the frame.
Change transform on an element with no layer of its ownPaintPaint, compositeWith no separate layer to move, the content has to be redrawn in its new position inside the layer it shares.
Scroll a scroller the compositor ownsCompositeComposite, plus paint for newly exposed tilesScrolling is a transform on already-rasterised tiles until content arrives that was never rastered.
Read offsetHeight after a writeLayout, immediatelyStyle and layout, synchronously, inside your taskThe value must be correct now, so the browser cannot defer the work to the frame boundary.

A frame is a deadline, not a function call

The browser aims to produce one frame per display refresh. Everything it does in between — running your task, draining microtasks, calling animation callbacks, recalculating style, laying out, painting, committing to the compositor — has to fit before the next one is due, or the frame is simply not there.

This is why total time is the wrong unit for interaction work. A given amount of script spread across five frames, yielding between them, is invisible; the same work in one uninterruptible task drops frames and delays the response to a click that arrived halfway through it (Yielding and Scheduling). It is also why the pipeline stages are worth naming individually: a frame that misses because layout ran on 4,000 rows has a different fix from one that misses because a full-screen blur was rasterised again.

What one frame containsrelative units — proportions of a single frame, not milliseconds
Task: input handler, timer or network callback runs
Microtask checkpoint drains
requestAnimationFrame callbacks
Recalculate style for the invalidated set
Layout for dirty subtrees
Paint: record display lists
Commit and composite
Whatever is left before the next refresh
  • Task: input handler, timer or network callback runsRuns to completion. Nothing else on this thread happens meanwhile.
  • Microtask checkpoint drainsPromise continuations. Still no rendering opportunity — a promise chain does not yield a frame.
  • requestAnimationFrame callbacksThe last chance to mutate before this frame's style and layout run.
  • Recalculate style for the invalidated setResizeObserver callbacks also run in these steps and can dirty style again; the loop is bounded.
  • Layout for dirty subtreesSkipped entirely if nothing geometric was invalidated.
  • Commit and compositeLargely off the main thread, which is why a busy main thread can still scroll.
  • Whatever is left before the next refreshIdle callbacks live here. On a slow device this is often negative — the frame is already late.

The proportions here are illustrative shape, not measurement: on a real page any one of these rows can dominate all the others. The ordering is what transfers.

How to build it

Most important first.

  • Ask which stage a change enters at before optimising anything. "This is slow" is not a diagnosis; "this triggers layout on 4,000 rows every keystroke" is, and it names its own fix (Measure Before Optimising).
  • Batch reads and writes. Do all layout reads, then all writes, so the browser resolves layout once per frame instead of once per read (Layout Thrashing).
  • Prefer changes that enter late in the pipeline for anything that happens per frame. Animating a compositor-friendly property is not a universal rule, but for an update that happens on every frame it is usually the difference between smooth and not (Cheap and Expensive Animation).
  • Bound the invalidated set structurally. Containment, content-visibility, and a smaller DOM all reduce the number of elements each stage has to consider (CSS Containment, content-visibility).
  • Reduce how much there is to style and lay out at all before micro-optimising each stage. Virtualising a long list removes work from every stage simultaneously (List Virtualization).
  • Treat the pipeline as the shared vocabulary between you and devtools. The Performance panel names these stages; a mental model that matches the trace is worth more than any rule of thumb (Debugging Rendering and Jank).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The accessibility tree is derived from the DOM and computed style, so it is downstream of the same pipeline. Anything that delays style and layout delays the moment assistive technology can perceive what changed (The Accessibility Tree).
  • A blocked main thread blocks assistive technology completely — focus moves late, live-region announcements queue, and a screen-reader user gets no visual cue that the page is busy, only silence (Long Tasks).
  • Visual-only changes announce nothing. Moving a thing with transform is invisible to the accessibility tree, so a state change communicated purely by position — a toggle sliding, a progress bar filling — needs the state expressed in semantics too (Semantics Before ARIA).
  • visibility and display are pipeline properties with accessibility consequences: display: none and visibility: hidden both remove the subtree from the accessibility tree, while opacity: 0 does not — content faded out is still readable by a screen reader and still focusable (Focus Management).
  • Animation that recruits the compositor is still animation. Honour the reduced-motion preference regardless of how cheap the animation is to render (Contrast, Colour and Motion).

What can go wrong

Failure modes
  • Forced synchronous layout: a read after a write, in a loop, so layout runs once per iteration and the frame is spent entirely inside your own event handler.
  • Layout that propagates further than expected — a percentage-sized child forcing a parent to resize, which resizes its siblings, because nothing bounded the subtree.
  • Paint storms: a large blurred shadow or filter over a big area redrawn every frame, so the main thread is idle and the frame still misses.
  • Layer explosion: will-change applied broadly as a "make it fast" incantation, producing hundreds of layers, each with its own memory and its own raster cost.
  • The mitigation failing: promoting an element to its own layer to make an animation cheap, then animating a layout property on it anyway, so you now pay layout *and* the layer.
  • Assuming the compositor path exists when it does not. An element with no composited layer animating opacity still repaints its contents every frame.
What can arrive out of order
  • A web font finishing its load after first paint re-runs style and layout for every element using it, which is why text can shift after it is already readable (Images and Fonts).
  • Image decode completes off the main thread and lands in whichever frame it is ready for, so an image can appear one frame after the layout that reserved space for it.
  • ResizeObserver callbacks run inside the rendering steps and can dirty style again; the loop is bounded, and exceeding the bound produces an error rather than an infinite frame.
  • IntersectionObserver delivers asynchronously, so "is this on screen" is always an answer about a previous frame — code that assumes it is current will fight the scroll.
Security
  • The pipeline is a side channel, and the browser deliberately cripples parts of it for that reason: :visited styling is restricted to a small set of properties and getComputedStyle lies about it, precisely because rendering differences leak history.
  • Cross-origin iframes are laid out and painted in isolation from your document; you cannot read their geometry or their pixels, and that isolation is a security boundary, not a performance quirk (The Browser Security Model).
  • Injected CSS is an XSS-class problem even without script: attacker-controlled style can move, cover or hide interface elements, and selectors that trigger background requests can exfiltrate attribute values (Cross-Site Scripting, Content Security Policy).
  • Third-party CSS and fonts loaded into your document participate in your pipeline with your page's authority — they can block first paint, force relayout, and observe nothing but affect everything (Third-Party Scripts and the Supply Chain).
  • Content that arrives from users can make rendering pathological on purpose: deeply nested markup, enormous inline text, or thousands of nodes are a client-side denial of service that no server-side rate limit sees (Sanitization and Trusted HTML).
Misreads
  • "Paint means pixels on the screen." Paint records drawing commands; rasterisation and compositing turn them into pixels, often elsewhere and later.
  • "Every DOM change causes a reflow." Most DOM changes mark things dirty and cost nothing until the browser resolves them once, before the next frame. What forces a reflow early is reading geometry back.
  • "The compositor makes things fast." The compositor makes a narrow class of changes cheap under specific conditions. Everything else still walks the whole pipeline (Compositing Layers).
  • "Fewer DOM nodes is always the answer." Fewer nodes reduce style and layout work, but a page can be slow with 200 nodes if it repaints a full-screen blur every frame.
  • "React re-renders and then the browser paints, so the framework controls the pipeline." A framework decides which DOM mutations to make. Everything after that mutation is the browser's, on the browser's schedule (What a Component Costs to Render).

Measuring it, and what changes in the field

How you would see this
  • The Performance panel is the pipeline, recorded: style recalculation, layout, paint and compositing appear as separately labelled events under the main thread flame chart (A Mental Model of the Devtools).
  • Forced layout shows up as a layout event nested inside your own function call rather than at the end of the frame — devtools flags it explicitly in most browsers.
  • The rendering overlay tools — paint flashing, layer borders, frame rate meters — answer "is this repainting?" faster than any trace reading (Debugging Rendering and Jank).
  • In the field, the interaction-responsiveness vital captures the whole chain from input to the next frame, which is the number that matches what the user felt (Interaction Responsiveness, Vitals in the Field).
  • Frame-level APIs report how long the browser spent producing a frame and what it spent it on; names and availability differ by engine, so feature-detect rather than assume.
Slow device, slow network, large data, old tab
  • On a slow device every main-thread stage costs proportionally more, and the compositor advantage grows: the gap between a compositor-only animation and a layout-driven one is small on a desktop and enormous on a mid-range phone (The Frame Budget).
  • On a high-density display the same CSS area is several times as many physical pixels to rasterise, so paint-heavy interfaces degrade on exactly the devices marketed as premium.
  • With a large DOM, style and layout costs scale with the number of elements the engine must consider — which is why the same code is instant on the demo page and unusable on the customer with 8,000 rows.
  • On a slow network the pipeline stalls before it starts: rendering is blocked until the render-blocking stylesheets have been parsed, regardless of how cheap your updates are afterwards (Render-Blocking Resources).
  • In a long-lived tab, layer count and retained DOM accumulate, so the same interaction that was smooth on load can miss frames an hour later.
What this costs
  • Reasoning in stages is slower than following a rule of thumb, and it requires reading traces. The payoff is that it generalises: the rules of thumb are wrong at exactly the moments performance work matters.
  • Optimising for the compositor pushes you toward transform and opacity, which cannot express every design. Some things genuinely need layout, and forcing them into transforms produces distorted text and blurry edges.
  • Bounding invalidation with containment and content-visibility buys pipeline savings with correctness risk — clipped overflow, collapsed boxes, content the user cannot find (CSS Containment).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe stage order — style before layout before paint before composite — and the rule that a change entering late skips the earlier stages hold across Blink, Gecko and WebKit, because they follow from the CSS specifications rather than from any implementation.
  • ENGINE-SPECIFICThe names and the sub-stage boundaries differ: Blink inserts a pre-paint and layerisation step and labels the trace events "Recalculate Style" and "Layout", Gecko historically calls layout "reflow" and frame construction its own stage, and WebKit talks about style resolution over a render tree. A trace from one browser will not use another's vocabulary.
  • SIMPLIFIEDThe five-box model deliberately hides layerisation, display-list building, tiling, raster scheduling and the commit handshake to the compositor thread. Those matter when you are debugging a specific frame, and they are where the engines differ most.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Computer Architecturecpu-vs-gpugpu-architecture
Domains that do not exist yet
  • Programming Languages & Runtime Internals — how the JavaScript engine compiles and optimises the handler that runs at the start of every one of these frames, and when the garbage collector takes a slice of the same budget.