Beyond Code & Karma · Browser Internals

The Browser Rendering Pipeline

From formless bytes to illuminated pixels — how Vishwakarma's forge works inside your browser.

Interactive Deep Dive
Parse HTML Build CSSOM Render Tree Layout Paint Composite

You type a URL, hit enter. 50ms later, pixels appear. What actually happened? The browser, like Vishwakarma, constructs an entire universe from formless bytes — and it does it in stages.

— The Pipeline, every page load

In Hindu mythology, Vishwakarma — the divine architect and craftsman of the gods — does not conjure the universe in a single instant. He works through stages: conceiving the form, establishing the laws of proportion, laying out structure, then adding colour and luminance. The browser's rendering pipeline is a perfect technological parallel. HTML arrives as undifferentiated bytes. The pipeline imposes structure, law, geometry, paint, and finally composites it all into the world you see. Each stage has its own dharma. Skip one or disrupt it and the entire manifestation breaks.

The Pipeline at a Glance

Every frame your browser renders passes through up to six sequential stages. A change to width restarts from Layout. A change to color restarts from Paint. A change to transform runs only the final stage — Composite. Understanding this is the foundation of all browser performance work.

01 Parse HTML → DOM
02 Build CSSOM → Style Rules
03 Render Tree DOM + CSSOM
04 Layout Geometry
05 Paint Display Lists
06 Composite GPU Layers

Each Stage, Explained

Let's walk through each stage in detail — what the browser actually does, what triggers it, and what the mythological parallel reveals about its nature.

Stage 01

HTML Parsing → DOM

🪷 Vedic Parallel

The universe begins as formless sound — Nada Brahma, the primordial vibration. The parser is the force that gives structure to those undifferentiated bytes, weaving them into a tree of meaning.

When the browser receives HTML bytes from the network, it doesn't see HTML — it sees a stream of bytes. The first job is to convert those bytes into characters (via the charset declared in the HTTP header or <meta charset>), then into tokens (start tags, end tags, attribute values, text nodes), and finally into nodes arranged in the Document Object Model (DOM) tree.

Parsing is incremental. The browser doesn't wait for the full HTML to arrive before beginning to parse. It parses in chunks as bytes arrive over the network — this is why you sometimes see partial page renders on slow connections. The tokenizer runs as a state machine, and the tree builder takes those tokens and constructs parent-child relationships.

The critical performance landmine here is the parser-blocking script. When the parser encounters a <script> tag without defer or async, it stops — completely — until that script downloads, parses, and executes. This is because scripts may call document.write() and fundamentally alter the HTML stream. The browser cannot know in advance, so it stops. This is why defer is almost always the right choice.

script-loading.html
<!-- ❌ Parser-blocking: halts HTML parsing until script runs -->
<script src="heavy.js"></script>

<!-- ✅ Deferred: parses in parallel, runs after DOM is ready -->
<script src="heavy.js" defer></script>

<!-- ✅ Async: fetches in parallel, runs as soon as available -->
<script src="analytics.js" async></script>
Stage 02

CSS Parsing → CSSOM

🌿 Vedic Parallel

Dharma — the laws of nature. Invisible rules that govern how everything manifests. You cannot see gravity, yet it shapes every physical form. The CSSOM is the browser's dharma: unseen laws that determine the appearance of every node.

While the HTML parser builds the DOM, a parallel process handles CSS. Every stylesheet — whether inline <style> or external <link> — is parsed into the CSS Object Model (CSSOM): a tree of style rules indexed by selector.

Unlike HTML parsing, CSS parsing is not incremental. The browser needs the entire stylesheet before it can apply it, because later rules can override earlier ones. A rule at the bottom of your CSS can change the appearance of elements at the top. This makes CSS a render-blocking resource: the browser will not produce a rendered frame until all stylesheets have been fully downloaded and parsed.

Cascade and specificity are resolved during CSSOM construction. The browser computes which rules win for each selector by comparing specificity (inline styles beat IDs beat classes beat elements) and origin (author vs user-agent). The result is a complete map: "for this selector, these are the winning computed values."

The performance implication is stark: a stylesheet loaded late holds up the entire render. Place critical CSS in the <head>, use media attributes to skip non-applicable stylesheets, or inline critical CSS for zero render-blocking cost.

css-loading.html
<!-- ❌ Render-blocking: browser won't paint until this loads -->
<link rel="stylesheet" href="styles.css">

<!-- ✅ Non-blocking for print: won't block initial render -->
<link rel="stylesheet" href="print.css" media="print">

<!-- ✅ Async CSS pattern (non-blocking) -->
<link rel="preload" as="style" href="non-critical.css"
      onload="this.rel='stylesheet'">
Stage 03

Render Tree

🔮 Vedic Parallel

Maya — the veil of illusion. Not everything that exists is visible; not everything visible truly exists. The render tree is what actually manifests — the subset of the DOM that will be perceived, stripped of the unseen.

The Render Tree is constructed by combining the DOM and CSSOM. It contains only the nodes that will be rendered visually — hidden nodes are excluded. This is not the same as the DOM tree.

The rules of inclusion are precise: display: none removes the node entirely — it exists in the DOM but has no box in the render tree, occupies no space, triggers no geometry. visibility: hidden keeps the node in the render tree and preserves its space, but paints it as invisible. opacity: 0 keeps everything including interactivity — the element is fully present, just transparent.

The browser also inserts anonymous boxes — nodes not in the DOM that the layout engine needs for internal bookkeeping. When you wrap text directly inside a <div> alongside block children, the browser creates anonymous block boxes to normalize the tree. These are invisible to JavaScript but real to the rendering engine.

visibility.css
/* display:none — removed from render tree entirely */
.hidden { display: none; }  /* no box, no geometry */

/* visibility:hidden — in render tree, painted invisible */
.invisible { visibility: hidden; }  /* space preserved */

/* opacity:0 — fully composited, still interactive */
.transparent { opacity: 0; }  /* cheapest for animation */
Key insight: Only opacity: 0 avoids reflow and repaint when animated. display: none toggling is the most expensive — it triggers a full render tree rebuild, reflow, and repaint. Prefer visibility or opacity for hiding animated elements.
Stage 04

Layout (Reflow)

⚖️ Vedic Parallel

Karma in action — every DOM change ripples consequences through the entire pipeline. Like karma, layout changes propagate: move one element and every dependent element must recalculate its position. The universe of geometry must rebalance.

Layout (also called reflow) is where geometry is computed. The browser takes the render tree and calculates the exact position and size of every node — in device pixels, relative to the viewport. This means resolving percentages, computing auto margins, handling flex and grid algorithms, and respecting the CSS box model.

Layout is expensive because it's inherently sequential: a parent's size depends on its children; children's positions depend on their parent. Changes propagate. When you change width, the browser must recompute the width, then all children that depend on that width, then their children — a cascading recalculation.

Layout thrashing is the worst-case scenario: reading a layout property (like offsetHeight, getBoundingClientRect()) forces the browser to flush all pending style changes and compute layout synchronously. If you then modify layout and read again in a loop, you force a layout recalculation on every single iteration. This is one of the most common causes of janky animations.

layout-thrashing.js
// ❌ Layout thrashing — forces reflow in a loop
const items = document.querySelectorAll('.item');
items.forEach(item => {
  const h = item.offsetHeight;  // read → forces layout flush
  item.style.height = (h * 2) + 'px'; // write → invalidates layout
});

// ✅ Batch reads, then batch writes
const heights = [...items].map(el => el.offsetHeight); // read phase
items.forEach((item, i) => {
  item.style.height = (heights[i] * 2) + 'px'; // write phase
});
Stage 05

Paint

🎨 Vedic Parallel

The world taking colour and form. After Vishwakarma establishes proportion and structure, the painters arrive — filling in the hues, the textures, the gradients of manifest reality.

Once layout has computed where everything goes, Paint determines what it looks like. The browser creates display lists — ordered records of paint operations (fill a rectangle with this colour, draw this text at this position, clip to this region). These are not pixels yet; they are instructions for producing pixels.

Rasterization is the process of executing those instructions to produce actual pixels. Chrome uses Skia, a cross-platform 2D graphics library, for rasterization. On modern hardware, rasterization happens on the GPU: display lists are uploaded as GPU commands and executed in parallel.

The browser creates paint layers (not to be confused with compositor layers). Elements that might need independent repainting — like elements with z-index, positioned elements, or elements with certain CSS properties — may get their own paint layer. This allows the browser to repaint only the affected area rather than the entire page.

Crucially, paint can happen without layout. If you change color or background-color, the browser knows geometry has not changed — it can skip layout entirely and jump straight to repaint.

paint-triggers.css
/* These trigger PAINT ONLY — no layout recalculation */
.box {
  color: #fff;                           /* text colour → repaint */
  background-color: rgba(0,0,0,0.5);   /* bg → repaint */
  box-shadow: 0 4px 12px rgba(0,0,0,0.4); /* shadow → repaint */
  border-color: #3b82f6;               /* border colour → repaint */
  outline: 2px solid blue;             /* outline → repaint */
}

/* But these trigger LAYOUT + PAINT (size/position changed) */
.box--bad {
  border-width: 4px;   /* changes box model → reflow! */
  padding: 20px;        /* changes geometry → reflow! */
}
Stage 06

Composite

✨ Vedic Parallel

Moksha — liberation. The compositor thread runs free from the main thread, unburdened by JavaScript or layout calculations. It operates in a state of pure, unobstructed flow, assembling the final frame on the GPU.

The final stage assembles the painted layers into the final frame displayed on screen. The compositor thread takes the rasterized layer tiles and composites them together — handling scrolling, transforms, and opacity adjustments entirely on the GPU, without touching the main thread.

This is the key insight behind the "compositor-only properties" rule. transform and opacity can be processed entirely by the compositor: no JavaScript, no layout, no paint is needed. The compositor simply applies a matrix transformation or alpha value to an already-rasterized layer tile and composites it. At 60fps, this means 16ms per frame — and the compositor can hit that budget even while the main thread is busy running JavaScript.

Elements promoted to their own compositor layer (via will-change: transform, or naturally through 3D transforms, video, canvas, and some other triggers) get their own GPU texture. The compositor can move, scale, or fade that texture without asking the main thread for anything. This is why transform-based animations remain smooth even under main thread load.

composite-only.css
/* ✅ These hit the compositor thread only */
.animate-cheap {
  transform: translateX(100px);  /* compositor-only */
  opacity: 0.8;                   /* compositor-only */
}

/* will-change promotes element to its own compositor layer */
.will-animate {
  will-change: transform;         /* pre-promote to GPU layer */
}

/* ❌ These are NOT compositor-only — they trigger layout */
.animate-expensive {
  left: 100px;     /* triggers reflow on positioned elements */
  width: 200px;    /* triggers layout recalculation */
  margin: 20px;   /* triggers full layout pass */
}

Karma Yoga, Jnana Yoga, Bhakti Yoga

The Bhagavad Gita describes three paths to liberation: Karma Yoga (the path of action), Jnana Yoga (the path of knowledge), and Bhakti Yoga (the path of devotion). The rendering pipeline has its own three paths — and just like their Vedic counterparts, they have very different costs.

Karma Yoga — Reflow
⚠️ High Cost
The path of full action. Changing geometry (size, position, margin) forces the browser to recalculate layout, then repaint, then composite. Every dependent element must recompute its position. This is the most expensive path — avoid in animations.

Triggers all 6 stages. Examples: width, height, padding, margin, font-size, top/left on positioned elements.
Jnana Yoga — Repaint
🟡 Medium Cost
The path of knowledge — changing appearance without changing form. Colour, shadow, and background changes skip layout entirely. The browser knows geometry has not changed, so it jumps to paint and composites from there.

Triggers stages 5–6. Examples: color, background-color, box-shadow, border-color, outline.
Bhakti Yoga — Composite
✅ Low Cost
The path of pure devotion — effortless, free from the burden of the main thread. Transform and opacity changes run entirely on the compositor thread on the GPU. No layout, no paint. The smoothest possible animation path.

Triggers stage 6 only. Examples: transform, opacity.

Here's the reference table every frontend engineer should have memorised. When you're choosing how to animate a property, check which path it triggers:

CSS Property Path Cost
width, height, margin, padding Reflow ⚠️ High
font-size, line-height, min-width Reflow ⚠️ High
top, left, right, bottom (positioned) Reflow ⚠️ High
color, background-color Repaint 🟡 Medium
box-shadow, text-shadow Repaint 🟡 Medium
border-color, outline Repaint 🟡 Medium
transform Composite ✅ Low
opacity Composite ✅ Low
The Animation Rule: Always prefer transform over top/left for movement. Always prefer opacity over visibility for fade effects. These two properties are the only ones that can animate at 60fps even when the main thread is under load.

Blink & GPU Internals

The six stages are an abstraction. Under the hood, Chrome's Blink engine has a more nuanced architecture involving multiple threads, independent layer trees, and tile-based rasterization. If you want to understand why compositor-only animations work, this is the answer.

Inspecting the Pipeline in DevTools

The rendering pipeline isn't abstract — you can watch it execute in real time using Chrome DevTools. Here's exactly where to look.

Chrome DevTools: Performance Panel

Open DevTools (F12 or Cmd+Option+I) and navigate to the Performance tab.

  1. Click the record button, interact with your page, then stop recording.
  2. In the flame chart, look for the Main thread track — you'll see colour-coded tasks: Parse HTML (blue), Recalculate Style (purple), Layout (yellow/orange), Update Layer Tree (teal).
  3. The Frames track at the top shows each rendered frame — green frames are on-budget (≤16ms), red frames are dropped.
  4. Expand any task to see its call stack — you can trace exactly which JavaScript function triggered a reflow.

Paint Flashing: In DevTools, go to More tools → Rendering (three-dot menu) and enable Paint Flashing. Regions that are being repainted flash green. Scroll your page — if you see the entire viewport flashing, you have paint performance issues. Elements that repaint on scroll should be on their own compositor layer.

Layer Borders: Also in the Rendering panel, enable Layer Borders to see orange borders around compositor layers and blue borders around tiles. This reveals exactly which elements have been promoted to GPU layers.

See it live

Step through each rendering stage interactively — parse HTML, build CSSOM, construct the render tree, compute layout, generate paint commands, and composite. Choose a scenario and watch the pipeline execute.

Open Visualizer →
JS Visualized Series
More deep dives on browser & JavaScript internals