The Critical Rendering Path
The Critical Rendering Path (CRP) is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on the screen. Every performance optimization you've ever done β code splitting, font loading, CSS inlining β is ultimately about shortening or unblocking this path.
The Pipeline
Network
β
βββββββΌββββββ βββββββββββββββ
β HTML ββββββββ DOM Tree β
β Parsing β ββββββββ¬βββββββ
ββββββββββββββ β
β merge
ββββββββββββββ ββββββββΌβββββββ
β CSS ββββββββ CSSOM Tree β
β Parsing β ββββββββ¬βββββββ
ββββββββββββββ β
βββββββΌβββββββ
β Render Treeβ (DOM + CSSOM, visible elements only)
βββββββ¬βββββββ
β
βββββββΌβββββββ
β Layout β (geometry: positions and sizes)
βββββββ¬βββββββ
β
βββββββΌβββββββ
β Paint β (pixels: colors, shadows, text)
βββββββ¬βββββββ
β
βββββββΌβββββββ
β Composite β (GPU layers merged to screen)
ββββββββββββββStep 1: HTML Parsing β DOM Construction
The browser parses HTML incrementally β it doesn't wait for the entire document. As bytes arrive, the parser tokenizes them and builds the DOM tree:
Bytes β Characters β Tokens β Nodes β DOM Tree
<html> HTMLHtmlElement
<head> βββ HTMLHeadElement
<title> β βββ HTMLTitleElement
<body> βββ HTMLBodyElement
<h1> βββ HTMLHeadingElement
<p> βββ HTMLParagraphElementParser Blocking
The HTML parser stops when it encounters:
<script>(without async/defer) β Parser halts, downloads the script, executes it, then resumes. The script might calldocument.write(), so the parser can't continue until it knows the script won't modify the document structure.<script>that depends on CSS β If a stylesheet is still loading and the script might read computed styles, the parser waits for CSS too. This creates a chain: CSS blocks JS, JS blocks HTML.
<!-- β Parser-blocking: stops everything -->
<script src="app.js"></script>
<!-- β
async: downloads in parallel, executes when ready (blocks parser briefly) -->
<script async src="analytics.js"></script>
<!-- β
defer: downloads in parallel, executes after parsing completes -->
<script defer src="app.js"></script>async vs defer:
| async | defer | |
|---|---|---|
| Download | Parallel | Parallel |
| Execute | As soon as downloaded (pauses parser) | After HTML parsing completes |
| Order | Not guaranteed | Guaranteed (document order) |
| Use for | Independent scripts (analytics, ads) | App scripts that depend on DOM or each other |
Step 2: CSS Parsing β CSSOM Construction
CSS is render-blocking β the browser won't render anything until all CSS is parsed. Why? Because the browser can't know what any element looks like until all stylesheets are processed (later rules can override earlier ones).
body { color: black; } β CSSOM node: body
h1 { font-size: 2em; } β CSSOM node: h1 (inherits body)
.highlight { color: red; } β CSSOM node: .highlightCSSOM is the Silent Bottleneck
CSS doesn't block DOM construction, but it blocks:
- Rendering β Nothing paints until CSSOM is complete
- JavaScript execution β Scripts that read styles wait for CSSOM
- First Contentful Paint β Any CSS in
<head>delays FCP
<!-- Every stylesheet in <head> is render-blocking -->
<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="typography.css">
<link rel="stylesheet" href="theme.css">
<!-- Optimization: inline critical CSS -->
<style>
/* Only what's needed for above-the-fold content */
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; display: flex; }
</style>
<!-- Defer non-critical CSS -->
<link rel="stylesheet" href="below-fold.css" media="print" onload="this.media='all'">Step 3: Render Tree Construction
The render tree is the merge of DOM and CSSOM β but only for visible elements:
DOM CSSOM Render Tree
html html { } html
βββ head head { } βββ body
β βββ meta (display: none) βββ h1 "Hello"
β βββ title βββ p "World"
βββ body body { color: #333 }
βββ h1 h1 { font-size: 2em }
βββ p p { }
βββ span.hidden .hidden { display: none } β NOT in render treeElements excluded from the render tree:
<head>,<meta>,<script>,<link>- Elements with
display: none(but NOTvisibility: hiddenoropacity: 0β those take up space)
Step 4: Layout (Reflow)
Layout computes the exact position and size of every element in the render tree. This is where percentages, ems, flex, and grid are resolved into pixel values.
Viewport: 1200px wide
body { margin: 0; padding: 20px; }
β Computed: x=0, y=0, width=1200, height=auto
.container { width: 80%; max-width: 960px; margin: 0 auto; }
β Computed: x=120, y=20, width=960
h1 { font-size: 2em; margin-bottom: 1em; }
β Computed: x=120, y=20, width=960, height=45Layout is expensive because it's a global operation β changing one element's size can cascade to every other element. This is called a reflow.
Step 5: Paint
Paint fills in pixels β colors, text, images, borders, shadows. The browser creates a list of draw calls:
Draw background for body (white)
Draw text "Hello World" at (120, 20) with font-size 32px
Draw border for .card at (120, 85) with 1px solid #ccc
Draw shadow for .card with blur 4pxModern browsers paint to layers β independent surfaces that can be composited by the GPU.
Step 6: Compositing
The compositor takes painted layers and combines them into the final image. This happens on the GPU and is extremely fast.
Elements that get their own compositor layer:
transformoropacityanimationswill-change: transformposition: fixed<video>,<canvas>,<iframe>
This is why transform and opacity animations are cheap β they only trigger compositing, skipping layout and paint entirely.
The Performance Cost Hierarchy
Most expensive Least expensive
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β Layout β β Paint β βComposite β β Nothing β
β (Reflow) ββ β (Repaint)ββ β (GPU) ββ β (Cache) β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ| Property Changed | Triggers |
|---|---|
width, height, margin, padding, top/left | Layout β Paint β Composite |
color, background, box-shadow, border-color | Paint β Composite |
transform, opacity | Composite only |
visibility: hidden | Paint β Composite |
Optimizing the Critical Rendering Path
1. Minimize Critical Resources
Resources that block first render:
- CSS in
<head> - Synchronous
<script>in<head>
Strategy: Inline critical CSS, defer everything else.
2. Minimize Critical Bytes
<!-- Preconnect to origins you'll need -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">3. Minimize Critical Path Length
The critical path length is the number of serial round-trips to get all critical resources:
Without optimization:
HTML β CSS (blocks render) β JS (blocks parser) β fonts β First Paint
4 round trips
With optimization:
HTML (with inline critical CSS + preload hints) β First Paint β async JS, fonts
1 round trip4. Font Loading Strategy
Fonts are a common CRP bottleneck:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
}font-display | Behavior |
|---|---|
auto | Browser decides (usually block) |
block | Hide text up to 3s, then swap (FOIT) |
swap | Show fallback immediately, swap when ready (FOUT) |
fallback | Brief invisible period (100ms), then fallback, swap if fast |
optional | Brief invisible period, use font only if already cached |
Measuring CRP Performance
Core Web Vitals Connection
| CRP Stage | Affects | Metric |
|---|---|---|
| DOM + CSSOM construction | Time to first render | FCP (First Contentful Paint) |
| Render tree + Layout + Paint | Largest visible element | LCP (Largest Contentful Paint) |
| Layout shifts during load | Visual stability | CLS (Cumulative Layout Shift) |
| Layout thrashing in event handlers | Input responsiveness | INP (Interaction to Next Paint) |
Performance API
const [navigation] = performance.getEntriesByType('navigation');
console.log({
domContentLoaded: navigation.domContentLoadedEventEnd,
domComplete: navigation.domComplete,
loadComplete: navigation.loadEventEnd,
});
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
console.log(`${entry.name}: ${entry.startTime}ms`);
});Interview Signal
Senior candidates demonstrate:
- Pipeline fluency β HTML β DOM, CSS β CSSOM, merge β Render Tree β Layout β Paint β Composite
- Blocking knowledge β CSS is render-blocking, sync JS is parser-blocking, and why
- Optimization strategy β Critical CSS inlining, defer/async scripts, preload hints, font-display
- Cost awareness β Layout is the most expensive, transform/opacity skip layout and paint
- Measurement capability β Core Web Vitals, Performance API, knowing what to profile