DNA🌐 Web BrowserThe Critical Rendering Path
πŸ¦–DinosaurBrowserPerformanceRendering

The Critical Rendering Path

Every pixel on screen is the result of a pipeline. Senior engineers who understand this pipeline don't guess at performance β€” they engineer it.

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>                 └── HTMLParagraphElement

Parser Blocking

The HTML parser stops when it encounters:

  • <script> (without async/defer) β€” Parser halts, downloads the script, executes it, then resumes. The script might call document.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:

asyncdefer
DownloadParallelParallel
ExecuteAs soon as downloaded (pauses parser)After HTML parsing completes
OrderNot guaranteedGuaranteed (document order)
Use forIndependent 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: .highlight

CSSOM is the Silent Bottleneck

CSS doesn't block DOM construction, but it blocks:

  1. Rendering β€” Nothing paints until CSSOM is complete
  2. JavaScript execution β€” Scripts that read styles wait for CSSOM
  3. 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 tree

Elements excluded from the render tree:

  • <head>, <meta>, <script>, <link>
  • Elements with display: none (but NOT visibility: hidden or opacity: 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=45

Layout 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 4px

Modern 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:

  • transform or opacity animations
  • will-change: transform
  • position: 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 ChangedTriggers
width, height, margin, padding, top/leftLayout β†’ Paint β†’ Composite
color, background, box-shadow, border-colorPaint β†’ Composite
transform, opacityComposite only
visibility: hiddenPaint β†’ 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 trip

4. 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-displayBehavior
autoBrowser decides (usually block)
blockHide text up to 3s, then swap (FOIT)
swapShow fallback immediately, swap when ready (FOUT)
fallbackBrief invisible period (100ms), then fallback, swap if fast
optionalBrief invisible period, use font only if already cached

Measuring CRP Performance

Core Web Vitals Connection

CRP StageAffectsMetric
DOM + CSSOM constructionTime to first renderFCP (First Contentful Paint)
Render tree + Layout + PaintLargest visible elementLCP (Largest Contentful Paint)
Layout shifts during loadVisual stabilityCLS (Cumulative Layout Shift)
Layout thrashing in event handlersInput responsivenessINP (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:

  1. Pipeline fluency β€” HTML β†’ DOM, CSS β†’ CSSOM, merge β†’ Render Tree β†’ Layout β†’ Paint β†’ Composite
  2. Blocking knowledge β€” CSS is render-blocking, sync JS is parser-blocking, and why
  3. Optimization strategy β€” Critical CSS inlining, defer/async scripts, preload hints, font-display
  4. Cost awareness β€” Layout is the most expensive, transform/opacity skip layout and paint
  5. Measurement capability β€” Core Web Vitals, Performance API, knowing what to profile