Guide

Browser critical rendering path explained

You ship HTML, CSS, and JavaScript. The browser must turn those bytes into pixels on screen — fast enough that readers do not bounce, and clean enough that Core Web Vitals stay green. The sequence of steps between network download and visible content is called the critical rendering path (CRP). Understanding it is the difference between guessing at performance tweaks and knowing why deferring a stylesheet or inlining 4 KB of CSS actually moves Largest Contentful Paint. This guide walks through each stage — DOM, CSSOM, render tree, layout, paint, and composite — what blocks rendering, and how modern browsers parallelize work you can still accidentally serialize.

What the critical rendering path is

The CRP is the minimum set of steps the browser must complete before it can paint the first frame of meaningful content. Not every downloaded byte is on the critical path — a analytics script at the bottom of the page may load in parallel without delaying first paint. But HTML in the <head>, blocking CSS, and synchronous JavaScript in the head typically are.

Browsers optimize aggressively: they stream HTML and start building the DOM before the full document arrives; they prefetch DNS and TLS connections; they cache compiled styles. Still, the fundamental dependency graph remains:

  1. Parse HTML into a DOM tree
  2. Parse CSS into a CSSOM tree
  3. Combine DOM + CSSOM into a render tree
  4. Run layout (calculate geometry)
  5. Paint pixels into layers
  6. Composite layers to the screen

JavaScript can interrupt at almost every step — reading layout properties triggers synchronous reflow; document.write can block parsing entirely. That is why the CRP and the JavaScript event loop are tightly coupled in real-world performance work.

Step 1: Building the DOM

When bytes arrive over the network, the HTML parser tokenizes tags and builds a tree of nodes — the Document Object Model. Each element, text node, and comment becomes a node with parent/child relationships. The parser is incremental: it does not wait for the entire file before processing the first tags.

Two things stall DOM construction:

  • Render-blocking CSS — when the parser hits a <link rel="stylesheet">, it must fetch and parse the stylesheet before painting, because later HTML might depend on those rules. The DOM keeps building, but rendering waits.
  • Parser-blocking JavaScript — a classic <script src="..."> without defer or async pauses HTML parsing until the script downloads and executes. The script may mutate the DOM before parsing resumes.

defer downloads in parallel and runs after DOM is ready. async downloads in parallel and runs as soon as it arrives — order is not guaranteed. For content sites and marketing pages, defer on non-critical scripts is almost always the right default.

Step 2: Building the CSSOM

CSS parsing produces the CSS Object Model — a separate tree where each selector maps to style declarations. Unlike DOM construction, CSSOM building is not fully incremental in the practical sense: the browser cannot determine whether .hero h1 { font-size: 2rem } applies until it has seen all rules, because later rules can override earlier ones (cascade and specificity).

That is why external stylesheets block rendering even when the DOM is ready. Inline <style> blocks in the head have the same effect — they must be parsed before first paint. Media queries help: <link media="print"> does not block screen rendering.

@import inside CSS is a performance trap — each import serializes another network round trip. Prefer multiple <link> tags or a bundled file at build time.

Step 3: Render tree and visibility

The render tree contains only nodes that will actually be painted. display: none elements are excluded. visibility: hidden and opacity: 0 may still occupy layout space depending on context, but invisible subtrees are pruned from painting work.

Each render tree node carries computed styles — the final cascade result after resolving inheritance, specificity, and !important rules. Text nodes become boxes with font metrics; replaced elements like <img> and <video> carry intrinsic dimensions that affect layout before the asset fully decodes.

This stage is where missing width and height on images causes layout shift: the browser reserves zero space, then expands when the image decodes — a direct hit to Cumulative Layout Shift (CLS). Our image optimization guide covers explicit dimensions and responsive srcset patterns that stabilize layout.

Step 4: Layout (reflow)

Layout — sometimes called reflow — calculates the exact position and size of every box in the render tree. The browser walks the tree, applying the CSS box model: margin, border, padding, content width. Flexbox and grid add constraint-solving passes that can be expensive on large documents.

Layout is recursive and directional: a parent cannot finalize height until children are measured, but percentage heights need a resolved parent height. Deeply nested flex containers with min-height: auto chains are a common source of unexpected reflow cost.

Reading certain properties after mutating the DOM forces synchronous layout — the browser must recalculate geometry immediately. Classic triggers include offsetWidth, getBoundingClientRect(), and getComputedStyle() after style changes. Batch DOM writes, then reads, in animation loops to avoid layout thrashing.

Step 5: Paint and composite

Once geometry is known, the browser paints — filling pixels for text, borders, backgrounds, and images. Painting happens into layers. Some layers are promoted to the GPU compositor: typically elements with transform, opacity animations, or will-change hints.

Composite is the final step: the compositor thread blends layers and presents the frame. Animations that only touch compositor-friendly properties (transform, opacity) can run at 60+ fps without triggering layout or paint on the main thread — a key technique for smooth UI.

Conversely, animating width, height, top, or margin forces layout and paint every frame — expensive on mobile GPUs. Prefer transform: translate() for movement and scale() for size changes when possible.

Paint cost hotspots

  • Large box shadows and blursbox-shadow and filter: blur() expand paint regions; frosted-glass effects using backdrop-filter are visually rich but GPU-heavy on dense pages.
  • Fixed/sticky headers over scrolling content — can invalidate large paint areas each scroll frame unless promoted to their own layer judiciously.
  • Custom fonts without fallback tuning — FOIT (flash of invisible text) delays LCP; font-display: swap with size-adjusted fallbacks reduces both invisible text and CLS. See our font optimization guide.

Render-blocking resources and the network

The CRP is not only about CPU — it is about the longest dependency chain on the network waterfall. A typical slow path looks like:

  1. HTML document (TTFB + download)
  2. Discover CSS in head → fetch CSS → parse CSSOM
  3. Discover fonts referenced in CSS → fetch fonts
  4. Discover hero image in body → fetch image → decode
  5. First meaningful paint

Each hop adds RTT. Mitigations that actually shorten the CRP:

  • Preconnect to font and CDN origins in <head>
  • Preload the LCP image and critical font files with <link rel="preload">
  • Inline critical CSS — the minimal rules needed for above-the-fold content, with the full stylesheet loaded async or deferred
  • HTTP caching — long-lived cache headers on static CSS/JS/fonts so repeat visits skip the network entirely; see HTTP caching explained
  • Compress and minify — Brotli/Gzip on text assets; smaller CSSOM parses faster

fetchpriority="high" on the LCP <img> tells the browser to prioritize that request over less important images — a small attribute with measurable LCP impact on image-heavy article pages.

How CRP connects to Core Web Vitals

Google's field metrics map directly onto CRP stages:

  • LCP (Largest Contentful Paint) — usually blocked by slow TTFB, render-blocking CSS, unoptimized hero images, or web fonts. Fix the longest network + parse dependency.
  • CLS (Cumulative Layout Shift) — unstable layout from missing image dimensions, late-injected ads, or font swaps without matched fallbacks. Fix at layout/render-tree stage.
  • INP (Interaction to Next Paint) — main-thread congestion from long JavaScript tasks during or after initial render. Defer non-critical JS and split heavy hydration.

Lab tools like Lighthouse simulate a cold cache on a throttled connection. Field data from CrUX reflects real caches and devices. Optimize the CRP for cold first visits; rely on caching for return traffic.

Measuring in DevTools

Chrome DevTools Performance panel records a timeline of parsing, scripting, layout, and paint events. Look for:

  • Long Parse HTML or Parse stylesheet blocks before first paint
  • Layout events clustered in tight loops (thrashing)
  • Recalculate style spikes after DOM mutations
  • Main-thread gaps where the compositor runs but content is blank — often font or CSS blocking

The Coverage tab shows unused CSS bytes — dead weight on the critical path. The Network panel waterfall reveals whether CSS or fonts serialize downloads. For content publishers, a 30-second recording of a cold load on "Slow 4G" is enough to identify the top bottleneck most days.

Optimization checklist

  • Keep HTML <head> lean — meta, preconnect, critical CSS, defer everything else
  • Never put parser-blocking <script> in head without defer
  • Inline or preload critical CSS; avoid @import chains
  • Set explicit width and height on images; use modern formats (WebP/AVIF)
  • Preload LCP image and primary web font; use font-display: swap
  • Animate transform and opacity, not layout properties
  • Cache static assets aggressively with immutable filenames
  • Audit third-party scripts — ad tags and analytics often extend the CRP silently

Key takeaways

  • The critical rendering path is DOM → CSSOM → render tree → layout → paint → composite.
  • CSS blocks rendering because the cascade must be resolved before paint.
  • Synchronous JavaScript in the head blocks HTML parsing — use defer.
  • Layout thrashing happens when JS interleaves DOM writes and geometry reads.
  • Compositor-only animations (transform, opacity) skip expensive layout/paint.
  • LCP, CLS, and INP are symptoms of specific CRP failures — diagnose the stage, not just the metric.

Related reading