Browser InternalsDOMCSSOMPerformanceCore Web Vitals

Rendering Pipeline, DOM & CSSOM

Understand how browsers turn raw HTML and CSS into pixels on screen. Master the rendering pipeline to optimize performance, ace interview questions, and build faster web applications.

30 min read12 sections
01

Overview

When you type a URL and hit enter, the browser doesn't just "show" the page. It runs a multi-step rendering pipeline that converts raw HTML and CSS into the pixels you see on screen.

The pipeline starts by parsing HTML into a DOM (Document Object Model) and CSS into a CSSOM (CSS Object Model). These two trees merge into a Render Tree, which the browser uses to calculate layout, paint pixels, and composite layers onto the screen.

Understanding this pipeline is essential for performance optimization. Metrics like FCP (First Contentful Paint) and LCP (Largest Contentful Paint) are directly tied to how efficiently the browser moves through these stages.

Why this matters

Every performance optimization you'll ever make — lazy loading, code splitting, CSS inlining, script deferring — maps back to one or more stages of this pipeline. Know the pipeline, and you'll know exactly where to optimize.

02

Full Rendering Pipeline

The browser rendering pipeline is a sequential process with six major stages. Each stage depends on the output of the previous one.

📄

Parse HTML

Build DOM

🎨

Parse CSS

Build CSSOM

🌳

Render Tree

DOM + CSSOM

📐

Layout

Calculate positions

🖌️

Paint

Fill pixels

🧩

Composite

Layer & display

1

HTML → DOM (Document Object Model)

The browser reads raw HTML bytes, converts them to characters, tokenizes them into tags, and builds a tree of nodes — the DOM. This tree represents the structure of your page.

2

CSS → CSSOM (CSS Object Model)

Similarly, the browser parses all CSS (external stylesheets, <style> tags, inline styles) and builds the CSSOM — a tree that maps styles to every node. CSS is render-blocking: the browser won't render until the CSSOM is complete.

3

DOM + CSSOM → Render Tree

The browser combines the DOM and CSSOM to create the Render Tree. Only visible elements are included — elements with display: none are excluded. The render tree knows both structure and style.

4

Layout (Reflow)

The browser walks the render tree and calculates the exact position and size of every element. This is where percentages become pixels, and flexbox/grid layouts are resolved.

5

Paint

The browser fills in actual pixels — text, colors, images, borders, shadows. This is the most visually intensive step. Complex styles (box-shadow, gradients) make painting slower.

6

Composite

The browser takes painted layers and composites them in the correct order. Elements with transform, opacity, or will-change get their own compositor layer, enabling GPU-accelerated animations.

Pipeline Flow (Simplified)text
BytesCharactersTokensNodesDOM

                                                    Render TreeLayoutPaintComposite

BytesCharactersTokensNodesCSSOM

Interview tip

When asked "What happens when you type a URL?" — the rendering pipeline is the second half of the answer (after DNS + HTTP). Being able to walk through all 6 stages clearly will set you apart.

03

DOM Deep Dive

The DOM is a tree-structured representation of your HTML document. Every HTML tag becomes a node in this tree. The browser exposes the DOM as a JavaScript API, which is how you interact with page content programmatically.

How the Browser Parses HTML

The HTML parser reads the document top-to-bottom. It converts raw bytes into a tree through four stages: bytes → characters → tokens → nodes → DOM tree.

example.htmlhtml
<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header>
      <h1>Hello World</h1>
    </header>
    <main>
      <p>Welcome to my site</p>
    </main>
  </body>
</html>
DOM Tree Structuretext
Document
 └── html
      ├── head
      │    ├── title
      │    │    └── "My Page"
      │    └── link[rel="stylesheet"]
      └── body
           ├── header
           │    └── h1
           │         └── "Hello World"
           └── main
                └── p
                     └── "Welcome to my site"

Script Blocking Behavior

When the HTML parser encounters a <script> tag, it stops parsing HTML and executes the script. This is because JavaScript can modify the DOM (via document.write()), so the parser can't safely continue until the script finishes.

script-blocking.htmlhtml
<!-- ❌ Parser-blocking: stops DOM construction -->
<script src="app.js"></script>

<!-- ✅ Async: downloads in parallel, executes when ready -->
<script src="analytics.js" async></script>

<!-- ✅ Defer: downloads in parallel, executes after DOM is built -->
<script src="app.js" defer></script>
🛑

Default (blocking)

Pauses HTML parsing. Downloads and executes immediately. Delays DOM construction.

async

Downloads in parallel with parsing. Executes as soon as downloaded — may interrupt parsing.

defer

Downloads in parallel. Executes only after the DOM is fully built. Maintains script order.

Best practice

Use defer for scripts that need the DOM (most app scripts). Use async for independent scripts like analytics. Avoid placing blocking scripts in the <head>.

04

CSSOM Explained

The CSSOM is the CSS equivalent of the DOM. The browser parses all CSS sources and builds a tree that maps computed styles to every element. Unlike the DOM, the CSSOM is render-blocking — the browser will not render a single pixel until the CSSOM is fully constructed.

How CSS Is Parsed

CSS parsing follows the same byte → character → token → node pattern as HTML. The browser resolves specificity, inheritance, and the cascade to compute the final style for every element.

styles.csscss
body {
  font-size: 16px;
  color: #333;
}

header {
  background: #f8f9fa;
  padding: 20px;
}

header h1 {
  font-size: 2em;    /* Inherits and computes to 32px */
  color: #111;       /* Overrides body color */
}

.hidden {
  display: none;     /* Excluded from render tree */
}
CSSOM Tree (Computed)text
body
 ├── font-size: 16px
 ├── color: #333

 ├── header
 │    ├── background: #f8f9fa
 │    ├── padding: 20px
 │    ├── font-size: 16px        (inherited)
 │    ├── color: #333            (inherited)
 │    │
 │    └── h1
 │         ├── font-size: 32px   (2em × 16px)
 │         └── color: #111       (overridden)

 └── .hidden
      └── display: none

DOM Blocking vs. Render Blocking

This distinction comes up frequently in interviews. Understanding it is key to optimizing page load performance.

AspectDOM Blocking (Scripts)Render Blocking (CSS)
What it blocksHTML parsing (DOM construction)Rendering (painting pixels)
Caused bySynchronous <script> tagsExternal CSS stylesheets
ImpactDelays DOM tree completionDelays first paint (FCP)
FixUse async / deferInline critical CSS, preload

Key insight

CSS doesn't block DOM construction — the parser keeps building the DOM while CSS downloads. But CSS does block rendering. The browser refuses to paint anything until it has the complete CSSOM, because rendering without styles would cause a flash of unstyled content (FOUC) like display: none.

05

Render Tree (DOM + CSSOM)

The render tree is the result of combining the DOM and CSSOM. It contains only the nodes that are actually visible on the page, along with their computed styles.

What Gets Included?

  • Visible DOM nodes with their computed styles
  • Pseudo-elements (::before, ::after) that generate content
  • Text nodes with inherited styles

What Gets Excluded?

  • Elements with display: none (completely removed)
  • <head>, <script>, <meta> and other non-visual tags
  • Elements hidden via HTML hidden attribute
visibility-comparison.htmlhtml
<!-- ❌ NOT in render treecompletely removed -->
<div style="display: none">Hidden from everything</div>

<!-- ✅ IN render treeinvisible but takes up space -->
<div style="visibility: hidden">Invisible but present</div>

<!-- ✅ IN render treetransparent but present -->
<div style="opacity: 0">Transparent but present</div>

Interview favorite

"What's the difference between display: none, visibility: hidden, and opacity: 0?" — display: none removes the element from the render tree entirely (no layout space). visibility: hidden keeps it in the render tree (takes space, but invisible). opacity: 0 keeps it fully interactive (can receive clicks) but transparent.

06

Layout (Reflow)

Layout (also called reflow) is the process where the browser calculates the exact position and size of every element in the render tree. It converts relative units (%, em, vh) into absolute pixel values.

When Does Layout Happen?

  • Initial page load (first layout)
  • Window resize
  • Adding or removing DOM elements
  • Changing element dimensions (width, height, padding, margin)
  • Changing font size or font family
  • Reading layout properties (offsetWidth, getBoundingClientRect())

Why Layout Is Expensive

Layout is one of the most expensive operations in the pipeline. A change to one element can trigger layout recalculation for its parent, siblings, and children. This cascading effect is why "layout thrashing" is a major performance killer.

layout-triggers.jsjavascript
// ❌ Layout thrashing — forces layout on every iteration
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
  // Reading offsetHeight forces a layout calculation
  const height = el.offsetHeight;
  // Writing style invalidates layout, forcing recalc next read
  el.style.height = (height * 2) + 'px';
});

// ✅ Batched reads and writes — single layout pass
const heights = [];
elements.forEach(el => {
  heights.push(el.offsetHeight);  // Batch all reads
});
elements.forEach((el, i) => {
  el.style.height = (heights[i] * 2) + 'px';  // Batch all writes
});
common-reflow-triggers.jsjavascript
// These properties/methods FORCE a synchronous layout:
element.offsetTop / offsetLeft / offsetWidth / offsetHeight
element.scrollTop / scrollLeft / scrollWidth / scrollHeight
element.clientTop / clientLeft / clientWidth / clientHeight
element.getBoundingClientRect()
window.getComputedStyle()

// These style changes TRIGGER layout:
width, height, padding, margin, border
top, left, right, bottom (positioned elements)
font-size, font-family, font-weight
display, position, float

Performance rule

Separate your DOM reads from DOM writes. Read all values first, then write all changes. This prevents the browser from recalculating layout multiple times in a single frame — a pattern known as "forced synchronous layout" or "layout thrashing."
top/left changes the elemen'’s layout position affecting other elements, while transform only applies a visual shift after layout, so it doesn’t trigger reflow.

07

Paint & Composite

Paint Phase

After layout, the browser knows where everything goes. The paint phase fills in the actual pixels — text rendering, colors, images, borders, shadows. The browser creates paint records (a list of draw calls) for each layer.

🟢

Cheap to Paint

Solid colors, simple text, basic borders. These are fast GPU operations.

🔴

Expensive to Paint

Box shadows, gradients, blur filters, complex clip-paths. These require more GPU computation.

paint-cost-comparison.csscss
/* 🟢 Cheap — simple color change, repaint only */
.button:hover {
  background-color: #3b82f6;
  color: white;
}

/* 🔴 Expensive — complex shadow triggers heavy paint */
.card:hover {
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
              0 0 0 1px rgba(0, 0, 0, 0.05);
  filter: blur(2px);
}

Composite Phase

The compositor takes painted layers and combines them in the correct stacking order. This is where the GPU shines. Elements promoted to their own compositor layer can be animated without triggering layout or paint.

gpu-accelerated-animations.csscss
/* ❌ Triggers layout + paint on every frame */
.animate-bad {
  transition: top 0.3s, left 0.3s;
}
.animate-bad:hover {
  top: 10px;
  left: 20px;
}

/* ✅ Compositor-only — GPU accelerated, no layout/paint */
.animate-good {
  transition: transform 0.3s, opacity 0.3s;
}
.animate-good:hover {
  transform: translate(20px, 10px);
  opacity: 0.8;
}

/* Promote to own layer for complex animations */
.animated-element {
  will-change: transform;
  /* or: transform: translateZ(0); */
}

The golden rule of animations

Only transform and opacity can be animated on the compositor thread without triggering layout or paint. Use these for 60fps animations. Everything else (width, height, top, left, margin) triggers expensive layout recalculations.

Reflow vs. Repaint Summary

Change TypeLayoutPaintComposite
width, height, margin✓ Yes✓ Yes✓ Yes
color, background, shadow✗ No✓ Yes✓ Yes
transform, opacity✗ No✗ No✓ Yes
08

Performance Insights

Now that you understand the pipeline, here are actionable optimizations mapped to each stage. These are the insights interviewers and real-world projects care about.

✓ Done

Avoid Layout Thrashing

Batch DOM reads before writes. Use requestAnimationFrame() to defer writes to the next frame. Libraries like FastDOM automate this pattern.

✓ Done

Minimize Reflows

Use CSS classes instead of inline style changes. Modify elements off-DOM (documentFragment), then append once. Avoid reading layout properties in loops.

✓ Done

Use transform Instead of top/left

Animations using transform and opacity skip layout and paint entirely. They run on the compositor thread, achieving smooth 60fps even on low-end devices.

→ Could add

Reduce Render-Blocking CSS

Inline critical above-the-fold CSS. Load non-critical CSS asynchronously with media queries or rel='preload'. Split CSS by route for code splitting.

→ Could add

Optimize DOM Size

Keep DOM under 1,500 nodes. Use virtualization for long lists. Avoid deeply nested structures — flat DOMs are faster to traverse and layout.

✓ Done

Defer Non-Critical Scripts

Use defer for app scripts and async for analytics/third-party. Move scripts to the bottom of <body> or use dynamic import() for code splitting.

→ Could add

Use will-change Sparingly

will-change: transform promotes elements to their own layer, enabling GPU acceleration. But overuse wastes memory. Apply only to elements that will actually animate.

Core Web Vitals connection

FCP (First Contentful Paint) is blocked by render-blocking CSS and synchronous scripts. LCP (Largest Contentful Paint) depends on how quickly the largest element is laid out and painted. CLS (Cumulative Layout Shift) is caused by unexpected reflows after initial render. Every optimization above directly improves one or more of these metrics.

09

Common Mistakes

These are real-world mistakes that even experienced developers make. Knowing them will help you write performant code and answer interview questions confidently.

🌳

Massive DOM Trees

Pages with 3,000+ DOM nodes are slow to layout, paint, and interact with. Every style recalculation walks the entire tree.

Keep DOM under 1,500 nodes. Use virtualization for lists. Lazy-load off-screen content.

🛑

Blocking Scripts in <head>

Placing <script src='bundle.js'> in the head blocks HTML parsing. The browser can't build the DOM until the script downloads and executes.

Use defer or async attributes. Place scripts at the end of <body>.

🔄

Layout Thrashing in Loops

Reading offsetHeight then setting style.height in a loop forces the browser to recalculate layout on every iteration — potentially hundreds of times per frame.

Batch all reads first, then batch all writes. Use requestAnimationFrame for DOM writes.

🎨

Animating Layout Properties

Animating width, height, top, or left triggers layout + paint on every frame. This causes jank, especially on mobile devices.

Use transform: translate() for movement and transform: scale() for size changes. These are compositor-only.

📦

Overusing will-change

Applying will-change to many elements creates excessive compositor layers, consuming GPU memory and actually hurting performance.

Apply will-change only to elements that will animate. Remove it after animation completes.

💅

Inline Styles in JavaScript

Setting styles one property at a time (el.style.width, el.style.height, el.style.margin) can trigger multiple reflows.

Use CSS classes (el.classList.add) or set cssText once. Let the browser batch the style changes.

10

Interview Questions

These questions are commonly asked in frontend interviews at top companies. Practice explaining each one clearly and concisely.

Q:Walk me through the browser rendering pipeline.

A: The browser parses HTML into a DOM tree and CSS into a CSSOM tree. These combine into a Render Tree (visible nodes only). The browser then runs Layout to calculate positions, Paint to fill pixels, and Composite to layer everything onto the screen. Each stage depends on the previous one.

Q:What is the difference between DOM and CSSOM?

A: The DOM represents the structure of the HTML document as a tree of nodes. The CSSOM represents the styles applied to those nodes. The DOM is built incrementally as HTML is parsed, while the CSSOM must be fully constructed before rendering can begin (render-blocking).

Q:What is the difference between reflow and repaint?

A: Reflow (layout) recalculates the geometry of elements — position, size, and how they affect neighbors. Repaint fills in visual properties like color and shadow without changing geometry. Reflow is more expensive because it always triggers repaint, but repaint doesn't trigger reflow.

Q:Why is CSS render-blocking?

A: The browser blocks rendering until the CSSOM is complete to prevent FOUC (Flash of Unstyled Content). If it rendered before CSS was ready, users would see raw HTML that suddenly jumps into styled layout — a terrible user experience. CSS doesn't block DOM parsing, only rendering.

Q:What triggers a layout/reflow?

A: Reading layout properties (offsetWidth, getBoundingClientRect), changing geometric styles (width, height, margin, padding), adding/removing DOM elements, changing font properties, and resizing the window. The browser tries to batch these, but reading a layout property forces an immediate synchronous layout.

Q:How do you achieve 60fps animations?

A: Only animate transform and opacity — these are the only properties that can be handled entirely by the compositor thread without triggering layout or paint. Use will-change to promote elements to their own layer. Avoid animating width, height, top, left, or any property that triggers layout.

Q:What is the render tree and what gets excluded from it?

A: The render tree is the combination of DOM and CSSOM, containing only visible elements with their computed styles. Elements with display: none, <head>, <script>, and <meta> tags are excluded. Elements with visibility: hidden ARE included (they take up space but aren't visible).

Q:Explain the difference between async and defer for scripts.

A: Both download scripts in parallel without blocking HTML parsing. async executes immediately when downloaded (may interrupt parsing, no guaranteed order). defer waits until the DOM is fully built, then executes in document order. Use defer for app scripts, async for independent scripts like analytics.

11

Practice Section

Test your understanding with these scenario-based questions. These simulate real interview follow-ups where you need to apply your knowledge.

1

Script in the <head>

What happens if you add <script src='heavy-bundle.js'></script> in the <head> without async or defer?

Answer: The HTML parser stops when it hits the script tag. It downloads and executes the entire bundle before continuing to parse the rest of the HTML. This means the DOM isn't built, the CSSOM can't combine with it, and the user sees a blank white screen until the script finishes. FCP is severely delayed. Fix: add defer to let parsing continue, or move the script to the end of <body>.

2

Reflow or Repaint?

You change an element's background-color from blue to red. Does this cause reflow, repaint, or both?

Answer: Repaint only. Changing background-color doesn't affect the element's geometry (size or position), so the browser skips the layout step. It only needs to repaint the affected pixels. This is much cheaper than a reflow. However, if you changed width or padding instead, that would trigger reflow + repaint.

3

Performance Optimization

A page has a list of 10,000 items, each with complex CSS (shadows, gradients). Scrolling is janky. How would you optimize?

Answer: Three key optimizations: (1) Virtualize the list — only render items visible in the viewport (react-window or @tanstack/virtual). This reduces DOM nodes from 10,000 to ~20. (2) Simplify CSS — replace box-shadows with simpler borders, use CSS containment (contain: layout style paint). (3) Use will-change: transform on the scroll container to promote it to a compositor layer for GPU-accelerated scrolling.

12

Cheat Sheet (Quick Revision)

One-screen summary for quick revision before interviews.

Quick Revision Cheat Sheet

Pipeline: HTML→DOM, CSS→CSSOM, Render Tree→Layout→Paint→Composite

DOM: Tree representation of HTML. Built incrementally. Scripts can block parsing.

CSSOM: Tree representation of CSS. Render-blocking — browser won't paint without it.

Render Tree: DOM + CSSOM merged. Only visible nodes. display:none excluded.

Layout (Reflow): Calculates position & size. Expensive. Triggered by geometry changes.

Paint: Fills pixels — colors, text, images. Triggered by visual-only changes.

Composite: Layers combined by GPU. transform & opacity are compositor-only (cheapest).

Reflow triggers: width, height, margin, padding, font-size, DOM add/remove, reading offsetWidth.

Repaint triggers: color, background, box-shadow, border-color, visibility.

Compositor-only: transform, opacity — skip layout & paint. Use for 60fps animations.

Script loading: default = blocking, async = parallel + immediate exec, defer = parallel + after DOM.

CSS optimization: Inline critical CSS, preload fonts, split by route, avoid @import.

DOM optimization: Keep under 1,500 nodes, virtualize lists, use documentFragment for batch inserts.

Layout thrashing: Read-write-read-write pattern. Fix: batch reads, then batch writes.