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:
- Parse HTML into a DOM tree
- Parse CSS into a CSSOM tree
- Combine DOM + CSSOM into a render tree
- Run layout (calculate geometry)
- Paint pixels into layers
- 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="...">withoutdeferorasyncpauses 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 blurs —
box-shadowandfilter: blur()expand paint regions; frosted-glass effects usingbackdrop-filterare 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: swapwith 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:
- HTML document (TTFB + download)
- Discover CSS in head → fetch CSS → parse CSSOM
- Discover fonts referenced in CSS → fetch fonts
- Discover hero image in body → fetch image → decode
- 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 withoutdefer - Inline or preload critical CSS; avoid @import chains
- Set explicit
widthandheighton images; use modern formats (WebP/AVIF) - Preload LCP image and primary web font; use
font-display: swap - Animate
transformandopacity, 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
- Core Web Vitals explained — LCP, INP, CLS thresholds and field vs lab data
- HTTP caching explained — Cache-Control, ETags, and CDN cache keys for static assets
- Web image optimization explained — srcset, lazy loading, and LCP hero image tactics
- JavaScript event loop explained — main-thread tasks, microtasks, and why long JS blocks paint