The Browser Rendering Pipeline (Full Picture)

Performance

Performance conversations often focus on individual metrics — LCP, CLS, INP — but every one of those metrics is a symptom of something happening inside the browser's rendering pipeline. The pipeline is the full sequence of work the browser performs between receiving a URL and putting pixels on screen. Each stage feeds into the next, and a bottleneck at any point delays everything downstream.

This article walks through every stage from network to compositing. If you want a focused look at the parse-to-paint portion and its optimisations, the Critical Rendering Path article covers that in detail.

Stage 1: Network

Before the browser can render anything, it needs bytes. The network phase is everything that happens between entering a URL and receiving the first byte of HTML.

DNS Resolution

The browser resolves the domain name to an IP address. This involves a recursive lookup through the DNS hierarchy — local cache, OS cache, resolver, root nameserver, TLD nameserver, authoritative nameserver. A cold DNS lookup can take 20–120 ms. Subsequent requests to the same domain reuse the cached result.

<!-- ✅ Resolve DNS for domains you'll need soon -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- ✅ Go further: resolve DNS + open TCP + TLS handshake -->
<link rel="preconnect" href="https://cdn.example.com" />

dns-prefetch resolves the domain ahead of time. preconnect goes further — it completes the full connection setup so the first real request to that origin has zero connection overhead.

TCP and TLS

After DNS, the browser opens a TCP connection (one round-trip for the three-way handshake) and, for HTTPS, negotiates TLS (one or two additional round-trips depending on the TLS version). TLS 1.3 reduces the handshake to a single round-trip; TLS 1.2 requires two.

On a 100 ms round-trip connection, TCP + TLS 1.2 costs 300 ms before any application data is exchanged. This is why connection reuse (HTTP keep-alive), HTTP/2 multiplexing (one connection for all requests to an origin), and HTTP/3 (QUIC, zero-RTT resumption) matter — they eliminate repeated handshake costs.

HTTP Request/Response and TTFB

The browser sends the HTTP request and waits. Time to First Byte (TTFB) measures the gap between request sent and first byte received. TTFB includes server processing time, database queries, server-side rendering, and the return trip over the wire.

The first byte of HTML kicks off the rest of the pipeline. Everything below depends on this byte arriving.

The Preload Scanner

While the main HTML parser processes tokens sequentially (and may be blocked by scripts), the browser runs a secondary parser called the preload scanner. This lightweight scanner reads ahead in the raw HTML looking for resource URLs — stylesheets, scripts, images, fonts — and starts fetching them before the main parser reaches those tags.

The preload scanner is why putting a <link rel="stylesheet"> in the <head> is effective: the scanner sees it immediately and starts the download, even while the main parser is still working through earlier elements. It is also why dynamically injected resources (created via JavaScript after page load) miss the benefit — they are invisible to the preload scanner.

Stage 2: Parse and Load

Once HTML bytes arrive, the browser begins building the data structures it needs to render.

HTML → DOM

The browser decodes HTML bytes into characters, tokenises them, and builds the Document Object Model (DOM) — a tree of nodes representing elements, attributes, and text. HTML parsing is incremental: the browser doesn't wait for the entire document. As soon as enough tokens are available, it starts building and can begin subsequent pipeline stages for the content it has already parsed.

CSS → CSSOM

When the parser encounters <link rel="stylesheet"> or <style> tags, it builds the CSS Object Model (CSSOM) — a tree representing all style rules. Unlike the DOM, the CSSOM must be complete before the browser proceeds to rendering. A later CSS rule can override an earlier one, so partial CSSOM would produce incorrect styles.

This is why CSS is render-blocking: any stylesheet referenced in the <head> delays the first paint until it is fully downloaded and parsed.

Scripts: Parser-Blocking vs Deferred

A <script> tag without attributes stops the HTML parser entirely. The browser must download the script, execute it, and only then resume parsing. JavaScript can modify both the DOM and CSSOM, so the browser cannot safely skip ahead.

<!-- ❌ Parser-blocking: stops everything until fetched + executed -->
<script src="app.js"></script>

<!-- ✅ defer: fetches in parallel, executes after HTML is fully parsed -->
<script src="app.js" defer></script>

<!-- ✅ async: fetches in parallel, executes as soon as ready -->
<script src="app.js" async></script>

defer preserves execution order and guarantees the DOM is fully built when the script runs — the right choice for most application scripts. async executes as soon as the download completes, regardless of parse state or other scripts — appropriate for independent scripts like analytics.

The difference matters at scale. A page with five synchronous scripts in the <head> can easily add a full second of blocking time. Adding defer to all five turns them into parallel downloads with zero parse blocking.

Stage 3: Style Computation

With the DOM and CSSOM in hand, the browser computes the final styles for every visible element.

Building the Render Tree

The DOM and CSSOM are combined into the render tree. Only visible elements are included. Elements with display: none are excluded entirely — they exist in the DOM but not in the render tree. Elements with visibility: hidden are included (they still occupy space) but are not painted.

Each node in the render tree carries its computed style: the final resolved values after cascading, inheritance, and specificity have been applied.

Selector Matching

The browser matches CSS selectors to DOM nodes. Selectors are evaluated right to left. For .sidebar .nav li a, the browser first finds all a elements, then checks each one to see if an ancestor is li, then nav, then .sidebar. Deeply nested selectors with broad rightmost parts (like * or div) force the browser to check more nodes.

In practice, selector matching is rarely a bottleneck on modern pages. But on pages with thousands of DOM nodes and thousands of CSS rules (large component libraries, complex dashboards), overly specific or universal selectors can contribute measurable time to this stage.

Forced Style Recalculation

Calling getComputedStyle(element) from JavaScript forces the browser to synchronously compute styles if any are dirty. This is similar to forced layout (discussed in Stage 4) — reading a computed value when styles have been modified forces work that the browser would prefer to defer.

Stage 4: Layout (Reflow)

Layout calculates the exact position and size of every node in the render tree.

What Layout Computes

The browser walks the render tree and resolves the box model for each node: content dimensions, padding, border, margin, and position relative to the viewport. It accounts for normal flow, flexbox, grid, absolute positioning, floats, and everything else that affects geometry.

Layout is an O(n) operation proportional to the number of nodes in the render tree. Changing the size of one element can trigger a cascade: a parent resizes, siblings shift, children adjust. This cascading cost is why layout is one of the most expensive pipeline stages.

Properties That Trigger Layout

Any CSS property that affects geometry triggers layout when changed:

  • Dimensions: width, height, min-width, max-height, padding, margin, border-width
  • Position: top, left, right, bottom, position
  • Display and flow: display, float, overflow, flex, grid
  • Text: font-size, font-weight, line-height, text-align

Forced Synchronous Layout

Reading geometric properties from JavaScript (offsetWidth, clientHeight, getBoundingClientRect(), scrollTop) forces the browser to synchronously complete any pending layout work to give you an accurate value. If you then write a style that invalidates layout and read again, you force layout twice.

// ❌ Forced layout on every iteration
elements.forEach((el) => {
  const height = el.offsetHeight // READ → forces layout
  el.style.height = height + 10 + 'px' // WRITE → invalidates layout
})

// ✅ Batch reads, then writes
const heights = Array.from(elements).map((el) => el.offsetHeight)
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'
})

This interleaved read-write pattern is called layout thrashing. For a detailed breakdown and fixes, see the Layout Thrashing article.

Stage 5: Paint

Paint converts the laid-out boxes into actual pixels.

What Happens During Paint

The browser traverses the render tree and generates paint records — a list of drawing instructions (draw this rectangle, fill this text, render this border-radius, apply this box-shadow). These instructions are grouped by layer.

Not every element gets its own layer. The browser creates layers for:

  • Elements with position: fixed or position: sticky
  • Elements with will-change: transform or will-change: opacity
  • Elements with a transform, opacity, or filter applied
  • Elements that overlap other composited layers
  • <video>, <canvas>, and elements using hardware-accelerated CSS

Each layer is painted independently, which is what makes compositing (Stage 6) possible.

Paint Complexity

Some CSS properties are more expensive to paint than others. box-shadow with large spread, complex border-radius, filter: blur(), and large background-image gradients all require more rasterisation work. On low-end devices, paint can become a visible bottleneck during animations.

Properties That Trigger Paint (but Not Layout)

Changing these properties skips layout and goes straight to paint:

  • color, background-color, background-image
  • box-shadow, border-radius, outline
  • visibility (toggling between visible and hidden)

These are cheaper than layout-triggering changes but still more expensive than compositor-only changes.

Stage 6: Composite

Compositing is the final stage — combining painted layers into the image the user sees.

How Compositing Works

Each painted layer is uploaded to the GPU as a texture. The compositor thread (separate from the main thread) takes these textures and combines them in the correct order, applying transforms, opacity, and clipping. The result is the final frame sent to the display.

Because compositing runs on the GPU and on a separate thread, it does not block the main thread. This is the key insight behind CSS animation performance.

Compositor-Only Properties

Two CSS properties can be changed without triggering layout or paint — the browser only needs to re-composite:

  • transform — translate, rotate, scale, skew
  • opacity — fade in, fade out

These animations run entirely on the compositor thread. The main thread is free to handle JavaScript, event processing, and other work. This is why the most consistent performance rule for CSS animations is: animate transform and opacity, avoid everything else.

/* ✅ Compositor-only: no layout, no paint */
.card {
  transition:
    transform 0.3s ease,
    opacity 0.3s ease;
}
.card:hover {
  transform: scale(1.05);
  opacity: 0.9;
}

/* ❌ Triggers layout + paint on every frame */
.card {
  transition:
    width 0.3s ease,
    margin-left 0.3s ease;
}
.card:hover {
  width: 110%;
  margin-left: -5%;
}

Layer Promotion with will-change

You can hint to the browser that an element will be animated by using will-change:

.animated-element {
  will-change: transform;
}

This promotes the element to its own compositor layer in advance, removing the cost of layer creation when the animation starts. Use it sparingly — every promoted layer consumes GPU memory. Promoting too many elements is worse than promoting none.

Which Phase Does Your CSS Trigger?

Not all CSS changes are equal. The phase a change triggers determines its cost:

Layout + Paint + Composite (most expensive):

  • width, height, padding, margin, top, left, font-size, display, position, float, flex, grid

Paint + Composite (moderate):

  • color, background, box-shadow, border-radius, outline, visibility

Composite only (cheapest):

  • transform, opacity

When you animate a property that triggers layout, the browser runs layout → paint → composite on every frame. At 60fps that is every 16.67 ms. If layout alone takes 10 ms, you have consumed most of the frame budget before paint even starts.

When you animate a compositor-only property, the main thread is not involved at all. The animation can sustain 60fps even while the main thread is executing JavaScript.

Measuring the Pipeline in DevTools

Chrome DevTools' Performance panel visualises the pipeline directly. Record a page load or interaction and look for:

  • Blue (Parse HTML) — time spent building the DOM.
  • Yellow (Evaluate Script) — time spent downloading, parsing, and executing JavaScript.
  • Purple (Recalculate Style + Layout) — style computation and layout work. Long purple bars indicate expensive reflows.
  • Green (Paint + Composite) — rasterisation and layer compositing. Tall green bars suggest complex paint operations.

Forced reflows appear as purple bars with a warning triangle inside a yellow (scripting) block. That pattern is layout thrashing — JavaScript forcing synchronous layout repeatedly.

The Rendering tab offers additional diagnostics: enable "Paint flashing" to see green overlays on areas being repainted, "Layout Shift Regions" to visualise CLS in real time, and "Layer borders" to see compositor layer boundaries.

Conclusion

The browser rendering pipeline is six stages: Network → Parse → Style → Layout → Paint → Composite. Every performance problem maps to one or more of these stages. Slow TTFB is a network problem. Render-blocking resources stall parsing. Expensive selectors slow style computation. Interleaved DOM reads and writes thrash layout. Complex visual properties inflate paint time. Animating geometry properties instead of transforms forces the entire pipeline to run on every frame. Knowing which stage a change triggers tells you exactly how expensive it is — and exactly where to look for the fix.