Introduction
The Critical Rendering Path (CRP) is the sequence of steps the browser performs to convert HTML, CSS, and JavaScript into visible pixels on the screen. Optimizing this pipeline directly impacts First Contentful Paint (FCP) and Largest Contentful Paint (LCP). This article breaks down each CRP stage — DOM construction, CSSOM construction, render tree, layout, paint, and composite — and provides actionable optimization strategies.
Stage 1: DOM Construction
When the browser receives HTML bytes, it:
- Byte → Characters: Decodes raw bytes into characters
- Tokenization: Converts characters into tokens (
StartTag,EndTag, etc.) - Lexing: Converts tokens into nodes
- DOM Tree: Builds a tree of nodes preserving parent-child relationships
<!-- This HTML produces a DOM tree with html > head + body > h1 + p -->
<!DOCTYPE html>
<html>
<head><title>Page</title></head>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
Optimization: Minimize HTML size, deliver early with proper server configuration, and use streaming (e.g., Transfer-Encoding: chunked).
Stage 2: CSSOM Construction
CSS is render-blocking by default. The browser must download and parse all CSS before rendering. CSS bytes go through the same transformation pipeline as HTML, producing the CSS Object Model (CSSOM).
body { font-size: 16px; }
h1 { color: blue; }
Each CSS rule is matched against selectors, and specificity is computed during this phase.
Optimizations:
- Inline critical CSS directly in
<head>for above-the-fold content - Defer non-critical CSS using
media="print"orloading="lazy" - Minify and use tree-shaking to remove unused CSS
Stage 3: Render Tree
The render tree combines DOM and CSSOM — it contains only visible elements. Elements with display: none or <head> children are excluded. Each render tree node contains its computed style.
DOM CSSOM
| |
+----+-----+
|
Render Tree
(visible nodes + computed styles)
Stage 4: Layout (Reflow)
The browser calculates the geometry (position and size) of each render tree node. This is a top-down pass starting from the viewport. Elements with percentage-based widths, flexbox, or grid require the browser to compute sizes relative to their containers.
Causes of Layout Thrashing
Repeated forced synchronous layouts — reading layout properties (e.g., offsetHeight) after writing styles — causes the browser to recalculate layout multiple times in a single frame:
// ❌ Bad: Forces layout on each iteration
for (let i = 0; i < boxes.length; i++) {
const width = box.offsetWidth; // reads (triggers layout)
box.style.width = (width + 10) + 'px'; // writes
}
// ✅ Good: Batch reads, then batch writes
const widths = boxes.map(b => b.offsetWidth); // reads
boxes.forEach((b, i) => b.style.width = (widths[i] + 10) + 'px'); // writes
Optimizations:
- Read layout properties first, then write
- Use
transformandopacityfor animations (compositor-only) - Avoid complex CSS selectors that trigger selector matching on reflow
- Use
contain: layout style paintto isolate subtrees
Stage 5: Paint
The browser fills pixels into layers — text, colors, images, borders, shadows. This is expensive because it involves rasterization. Elements on separate compositor layers can be painted independently.
Triggers for Paint
- Changes to
color,background-color,visibility,outline - Adding shadows or text decorations
Stage 6: Composite
The browser merges painted layers into the final frame on the GPU. Compositing is cheap relative to layout and paint. The key is to minimize layout and paint work so that most changes only require compositing.
Compositor-Only Properties
| Property | Triggers Layout | Triggers Paint | Compositor-Only |
|---|---|---|---|
transform | No | No | Yes |
opacity | No | No | Yes |
width | Yes | Yes | No |
top/left | Yes | Yes | No |
Blocking vs Non-Blocking Resources
| Resource | Default Behavior | Optimization |
|---|---|---|
CSS (<link>) | Render-blocking | Inline critical, defer non-critical |
JS <script> | Parser-blocking (DOM) | Use async or defer |
JS defer | Non-blocking, executes after HTML parse | Order preserved |
JS async | Non-blocking, executes when downloaded | No order guarantee |
<!-- ✅ Best practices -->
<script async src="analytics.js"></script>
<script defer src="app.js"></script>
<link rel="stylesheet" href="critical.css" />
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'" />
Measuring CRP Performance
Lighthouse provides CRP-specific audits:
- Eliminate render-blocking resources — identifies blocking CSS/JS
- Defer offscreen images — reduce paint area
- Minimize main-thread work — identifies long tasks
- Reduce JavaScript execution time — helps with parser-blocking scripts
Run: npx lighthouse https://example.com --preset=desktop
Conclusion
Optimizing the Critical Rendering Path requires understanding the sequential nature of DOM/CSSOM construction, render tree formation, layout, paint, and composite. Key strategies include inlining critical CSS, using async/defer for scripts, avoiding layout thrashing through batched DOM reads/writes, and preferring compositor-only properties for animations. Regular Lighthouse audits help identify CRP bottlenecks and guide continuous improvement.
