Skip to content

The Critical Rendering Path

advanced10 min read

The Pipeline That Paints Every Pixel

Every time you navigate to a URL, the browser executes a deterministic pipeline to turn raw bytes into the visual page you see. This pipeline has six stages, and understanding each one is the single most important thing you can learn about frontend performance.

Most performance advice you've heard — "minimize CSS," "defer scripts," "use transform instead of top" — are symptoms. The critical rendering path is the cause. Once you understand the pipeline, every optimization becomes obvious.

Mental Model

Think of the browser as a factory assembly line. Raw materials (HTML bytes) enter one end. At each station, workers transform them: bytes become characters, characters become tokens, tokens become a tree, the tree gets styled, positioned, painted, and finally composited into the image you see. If any station is slow or blocked, the entire line stops — nothing ships until every stage completes its work.

The Six Stages

The critical rendering path consists of six sequential stages:

  1. HTML Parsing — Bytes to DOM tree
  2. CSSOM Construction — CSS bytes to style tree
  3. Render Tree — DOM + CSSOM merged (only visible nodes)
  4. Layout (Reflow) — Calculate exact position and size of every element
  5. Paint — Fill in pixels: colors, text, shadows, borders
  6. Composite — Layer the painted results and send to GPU for display

Each stage depends on the previous one. The browser cannot paint before it knows the layout. It cannot compute layout without the render tree. And it cannot build the render tree without both the DOM and the CSSOM.

Bytes → Characters → Tokens → Nodes → DOM
                                        ↘
                                     Render Tree → Layout → Paint → Composite
                                        ↗
Bytes → Characters → Tokens → Nodes → CSSOM

Why CSS Blocks Rendering

CSS is render-blocking by default. The browser will not render a single pixel until it has fully parsed every CSS file linked in the <head>.

Why? Because any CSS rule could potentially change the appearance of any element on the page. If the browser painted before CSSOM was complete, users would see a flash of unstyled content (FOUC), then a jarring repaint.

<!-- This blocks rendering until styles.css is fully downloaded and parsed -->
<link rel="stylesheet" href="styles.css">

<!-- This does NOT block rendering for screens (only for print) -->
<link rel="stylesheet" href="print.css" media="print">

The media attribute is key. A stylesheet with media="print" is downloaded but does not block rendering for screen display. This is one of the simplest and most overlooked optimizations.

Media query trick

Split your CSS by media query. Styles for screen, print, and specific breakpoints can each be loaded with the appropriate media attribute, so only the CSS for the current context blocks rendering.

Why JavaScript Blocks Parsing

JavaScript is parser-blocking by default. When the HTML parser encounters a <script> tag without async or defer, it must:

  1. Stop parsing HTML
  2. Wait for the script to download (if external)
  3. Execute the script
  4. Resume parsing

This happens because JavaScript can modify the DOM via document.write() — the parser has no way to know in advance whether the script will change the document structure.

<!-- Parser-blocking: stops HTML parsing until executed -->
<script src="app.js"></script>

<!-- Async: downloads in parallel, executes when ready (blocks parsing briefly) -->
<script src="analytics.js" async></script>

<!-- Defer: downloads in parallel, executes after HTML parsing completes -->
<script src="app.js" defer></script>
Common Trap

async scripts execute as soon as they download, in no guaranteed order. If script B depends on script A, async can break your app when B downloads faster. Use defer when script order matters — deferred scripts always execute in document order, after HTML parsing completes.

The Render-Blocking Sequence

Here is the exact sequence the browser follows for the initial render:

Execution Trace
Step 1
Browser receives HTML bytes from the network
TCP stream delivers chunks
Step 2
HTML parser begins converting bytes → characters → tokens → DOM nodes
Incremental — doesn't wait for full document
Step 3
Parser hits `<link rel='stylesheet'>` — requests CSS file
CSS download starts, but parser continues building DOM
Step 4
Parser hits `<script src='app.js'>` — STOPS parsing
Parser blocked until script downloads AND executes
Step 5
Script needs CSSOM — waits for CSS download to complete first
JS that reads styles forces CSSOM completion
Step 6
CSS parsed → CSSOM built. Script executes. Parser resumes.
JS execution may modify the DOM
Step 7
DOM + CSSOM both complete → Render Tree constructed
Only visible elements included
Step 8
Layout → Paint → Composite → First pixels on screen
First Contentful Paint (FCP)

Notice the chain: CSS blocks JS (because JS might read computed styles), and JS blocks HTML parsing (because JS might modify the DOM). This CSS → JS → HTML blocking chain is the single biggest bottleneck on initial page load.

The CSSOM-JavaScript dependency

When JavaScript calls getComputedStyle(), offsetWidth, or any property that requires style information, the browser must have a complete CSSOM. If CSS is still downloading, JavaScript execution pauses until CSS is ready. This means a slow CSS download blocks JavaScript execution, which in turn blocks HTML parsing. The fix: load CSS early in <head>, minimize CSS file size, and avoid JavaScript that reads layout properties during initialization.

Measuring the Critical Path

Three metrics define the performance of your critical rendering path:

  1. Critical resources — Number of resources that block first render (CSS files, synchronous scripts)
  2. Critical path length — Number of network round trips to fetch all critical resources
  3. Critical bytes — Total bytes of all critical resources

The goal: minimize all three. Every CSS file you add to <head> is another critical resource. Every synchronous <script> extends the critical path.

<!-- BAD: 3 critical resources, potentially 3 round trips -->
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="layout.css">
<link rel="stylesheet" href="theme.css">
<script src="app.js"></script>

<!-- BETTER: 1 critical CSS resource, deferred JS -->
<link rel="stylesheet" href="bundle.css">
<script src="app.js" defer></script>

Production Scenario: The 3-Second Blank Screen

A production e-commerce site had a 3-second white screen on 3G connections. The root cause:

  1. A 200KB CSS file in <head> (render-blocking)
  2. A synchronous Google Tag Manager script before </head>
  3. The tag manager loaded 4 additional scripts, each parser-blocking

The fix:

  • Inlined critical CSS (above-the-fold styles) directly in <head> — 14KB
  • Loaded the full stylesheet with media="print" onload="this.media='all'" — non-blocking
  • Moved all scripts to defer
  • Result: First Contentful Paint dropped from 3.2s to 0.9s on 3G
What developers doWhat they should do
Put all CSS in one big file in `<head>`
Every byte of render-blocking CSS delays first paint
Inline critical CSS, async-load the rest
Use `<script>` in `<head>` without defer/async
Synchronous scripts block HTML parsing completely
Use defer for app scripts, async for independent scripts
Assume async and defer are the same
async scripts can execute out of order, breaking dependencies
async = execute immediately when downloaded (unordered). defer = execute after parsing (ordered)
Ignore CSS as a performance factor
Nothing renders until CSSOM is complete — CSS is on the critical path
Treat CSS as the primary render-blocking resource

Quiz: Critical Path Analysis

Quiz
A page has 2 CSS files and 1 synchronous script in <head>. The script reads element.offsetWidth. What is the minimum number of critical resources blocking first render?
Quiz
You move a <script> from <head> to just before </body> (without async/defer). What changes?
Key Rules
  1. 1CSS is render-blocking by default. Nothing paints until CSSOM is complete.
  2. 2JavaScript is parser-blocking by default. The HTML parser stops at every synchronous <script>.
  3. 3CSS blocks JavaScript when JS reads computed styles — creating a CSS → JS → HTML blocking chain.
  4. 4Use defer for scripts that need DOM access. Use async for scripts with no dependencies (analytics).
  5. 5Inline critical CSS (above-the-fold styles) and async-load the rest to eliminate the CSS bottleneck.
  6. 6Every additional critical resource adds to first render time. Minimize critical resources, critical path length, and critical bytes.

Interactive: The Rendering Pipeline

1/11