TL;DR / The critical rendering path is the sequence of steps — parse HTML, build DOM and CSSOM, construct render tree, compute layout, paint — that the browser must complete before it can display the first pixel.
How It Works
┌────────┐ ┌────────┐
│ HTML │───────→│ DOM │───┐
└────────┘ └────────┘ │ ┏━━━━━━━━━━━━━━┓ ┌──────────┐ ┌─────────┐
┌───→┃ Render Tree ┃───────→│ Layout │───────→│ Paint │
│ ┗━━━━━━━━━━━━━━┛ └──────────┘ └─────────┘
│
┌────────┐ ┌────────┐ │
│ CSS │───────→│ CSSOM │───┘
└────────┘ └────────┘
┌────────┐
│ JS │ can block DOM + CSSOM construction
└────────┘
The critical rendering path (CRP) is the minimum set of work the browser must perform to render the initial view of a page. Every millisecond on this path delays First Contentful Paint (FCP). Optimizing the CRP means reducing the amount of work, the number of network round-trips, and the time blocking resources spend on the critical path.
Step 1: HTML Parsing and DOM Construction
The browser receives HTML bytes over the network, decodes them into characters, tokenizes them according to the HTML spec, and builds the DOM tree. This process is incremental — the parser can start building the DOM before the full HTML has arrived. But the parser can be blocked by <script> tags (without async or defer) because scripts might call document.write() or modify the DOM structure being parsed.
Step 2: CSS Parsing and CSSOM Construction
As the parser encounters <link rel="stylesheet"> or <style> blocks, the browser fetches and parses CSS into the CSS Object Model (CSSOM). Unlike DOM construction, the CSSOM must be built completely before any rendering can occur — partial CSS could produce a flash of unstyled content (FOUC). This makes CSS render-blocking by default.
The CSSOM construction is not incremental in the way DOM construction is. The cascade requires the full stylesheet to determine final computed values, because a rule later in the stylesheet can override an earlier one.
Step 3: Render Tree Construction
The browser combines the DOM and CSSOM to create the render tree. This tree contains only the nodes that are visible on screen — elements with display: none are excluded, as are <head>, <script>, and other non-visual elements. Each node in the render tree carries its computed styles.
Visibility matters here: visibility: hidden elements are included in the render tree (they still take up space), while display: none elements are not.
Step 4: Layout (Reflow)
The browser walks the render tree and calculates the exact position and size of every element. This is the "box model" computation — width, height, padding, margin, and position for every visible node. Layout is influenced by the viewport size, font metrics, percentage-based dimensions, and flexbox/grid algorithms.
Layout is expensive and cascading. A change to one element's size can affect its siblings, parent, and children. This is why layout thrashing — forcing multiple synchronous layouts — is so damaging to performance.
Step 5: Paint
The browser converts the render tree into actual pixels. Painting fills in every visible pixel: text, colors, images, borders, shadows. Modern browsers break painting into layers — elements with certain CSS properties (position: fixed, will-change: transform, opacity animations) get their own compositing layers.
Paint is typically the most visually impactful step. After paint, the compositor takes the layers and assembles them into the final image displayed on screen.
Optimizing the Critical Path
Minimize critical resources. Reduce the number of CSS and synchronous JS files that block rendering. Inline critical CSS directly in <head> to avoid an extra network round-trip. Use async or defer on scripts.
Minimize critical bytes. Compress CSS and JS. Remove unused CSS. Minify everything on the critical path. Use modern image formats (WebP, AVIF) for above-the-fold images.
Minimize critical path length. The critical path length is the number of sequential network round-trips required before the browser can render. Each blocking resource that must be fetched adds a round-trip. HTTP/2 server push and preload hints (<link rel="preload">) reduce this by starting fetches earlier.
Defer non-critical work. Move non-essential CSS behind media queries (e.g., media="print") so it is not render-blocking. Lazy-load images below the fold. Split JavaScript bundles and load only what the initial view requires.
Measuring the CRP
Chrome DevTools' Performance tab shows the exact timeline: parse, style, layout, paint, and composite for each frame. Lighthouse reports CRP-related metrics including FCP, Largest Contentful Paint (LCP), and Total Blocking Time (TBT). The Coverage tab shows how much downloaded CSS and JS is actually used during initial render.
Gotchas
- CSS is render-blocking but not parser-blocking. The HTML parser continues while CSS is fetched, but no pixels are painted until the CSSOM is ready. Moving CSS to the end of
<body>causes FOUC, not faster rendering. - Inline CSS eliminates a round-trip but increases HTML size. For repeat visits, external CSS benefits from caching. The trade-off favors inlining only for critical above-the-fold styles.
@importin CSS creates chained requests. The browser discovers the imported file only after downloading and parsing the parent stylesheet. This adds a sequential round-trip to the critical path. Avoid@import; use<link>tags instead.- Fonts can block text rendering. If a custom font is on the critical path, the browser may show invisible text (FOIT) until it loads. Use
font-display: swapto show fallback text immediately. - Preloading everything defeats preloading.
<link rel="preload">should be used only for resources critical to the initial render. Preloading non-critical resources contends for bandwidth with actual critical resources.