Critical Rendering Path: How the Browser Draws the First Frame
What happens between 'received HTML' and 'user sees content'. A step-by-step breakdown and where time is typically lost.
Between the moment "the browser received the first byte of HTML" and the moment "the user sees content," a great deal of invisible work takes place. Understanding this chain — the Critical Rendering Path, CRP — is what separates an SEO specialist who optimizes Core Web Vitals by guesswork from one who knows exactly where to look for bottlenecks.
This article walks through the CRP step by step and draws practical conclusions for each stage.
CRP Stages
There are five of them:
- Construct the DOM — building the DOM tree from HTML
- Construct the CSSOM — building the style tree from CSS
- Render Tree — merging the DOM and CSSOM
- Layout — computing geometry (positions, element sizes)
- Paint + Composite — drawing pixels on screen
JavaScript slots in between these stages and can break everything. Let's go through each one.
Stage 1: DOM Construction
The browser receives HTML byte by byte. The parser converts tags into DOM tree nodes line by line.
What blocks it:
- Encounters a
<script>withoutasync/defer— stops parsing, downloads the JS, executes it, then resumes - Encounters
<link rel="stylesheet">— continues parsing, but cannot start rendering without the CSSOM - Encounters
<iframe>— opens a separate child context, does not block the main one
What to speed up:
deferorasyncon scripts (see render-blocking resources)- HTML streaming from the server (deliver HTML as it becomes ready, without waiting for the full page)
- Reducing HTML size (gzip/brotli compression)
A typical HTML document of 50–100 KB is parsed in 10–30 ms. This is rarely the bottleneck. The real bottleneck is usually downloading the HTML over the network.
Stage 2: CSSOM Construction
CSS blocks rendering. The browser must build a complete CSSOM before the first paint.
How it works:
- The browser encounters
<link rel="stylesheet" href="main.css">in the HTML - It fetches
main.cssin a separate request - It parses the CSS and builds the rule tree
- If the CSS contains
@import— it repeats the process recursively (slow — see @import in CSS)
What blocks it:
- Large CSS files (200+ KB uncompressed)
@importchains- CSS from external domains (requires DNS lookup, handshake)
- CSS blocked by a slow server
What to speed up:
- Inline critical CSS in HTML — removes blocking for above-the-fold content
- Preload the main CSS —
<link rel="preload" href="main.css" as="style"> - Brotli compression
- Removing unused CSS (PurgeCSS)
Stage 3: Render Tree
Once the DOM and CSSOM are ready, the browser builds the Render Tree — the DOM with computed styles applied to each node. Hidden elements (display: none) are excluded from the Render Tree.
What blocks it:
- DOM not yet ready (heavy HTML, synchronous scripts)
- CSSOM not yet ready (large CSS, @import)
The stage itself is fast (5–20 ms) — it simply waits for the DOM + CSSOM.
Stage 4: Layout (Reflow)
The browser computes the geometry of each node — where it will appear on screen, what size it will be, and how it relates to its neighbors.
What slows it down:
- Thousands of nodes in the DOM (a page with 50,000 elements takes a long time to calculate)
- Complex flexbox/grid with recalculation
- Viewport resize events
- JavaScript that changes geometry between paints (for example, dynamically adding elements)
What to speed up:
- Minimize the DOM — keep the structure simple, avoid unnecessary wrappers
- Use
contain: layout styleon isolated components — the browser will not recalculate everything when one block changes - Avoid "forced layouts" in JS — this happens when you modify an element and immediately read its geometry (
offsetWidth,clientHeight), forcing the browser to recalculate right away
Group reads and writes together:
// Bad — N reflows
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px'; // read+write on every element
});
// Good — 1 reflow
const widths = elements.map(el => el.offsetWidth); // all reads
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px'; // all writes
});
Stage 5: Paint + Composite
The final step — actually drawing pixels. The browser arranges elements into layers, renders each layer separately, then composites them into the final frame.
What slows it down:
- Large areas with heavy styles (shadows, filters, gradients)
- Animating
width,height,top,left— requires geometry recalculation backdrop-filter— the most expensive effect in modern CSS
What to speed up:
- Animate only with
transformandopacity— they run on the GPU and do not require a paint will-change: transformon animated elements — hints to the browser to allocate a separate compositor layer- Use
backdrop-filtersparingly (if truly needed — apply it to small areas only) - Use
content-visibility: autofor off-screen blocks — skips painting content that is not visible
Where JavaScript Fits In
JS can slot in at any point in the CRP and disrupt everything.
Synchronous JS in <head>:
Blocks DOM parsing. The parser stops, the JS is downloaded, parsed, and executed. Only then does the parser continue.
<head>
<script src="heavy.js"></script> <!-- blocks, bad -->
<script src="heavy.js" defer></script> <!-- waits for DOM, then executes -->
<script src="heavy.js" async></script> <!-- executes when ready, may interrupt -->
</head>
JS that manipulates the DOM:
If JS adds elements — Layout is recalculated. If it changes styles — Paint is recalculated. Minimize DOM manipulation from JS, especially during page load.
JS that measures geometry:
element.offsetWidth, getBoundingClientRect() — these trigger a "sync layout." They force the browser to recalculate Layout immediately. Calling them frequently causes significant overhead.
Metrics and Where to Look
In Chrome DevTools → Performance:
- Record a page load (3–5 seconds)
- Open the Main thread timeline
- The color coding shows:
- Yellow (Scripting) — JS
- Purple (Rendering) — Layout
- Green (Painting) — Paint
- Blue (Loading) — network
Wherever a color takes up a lot of space — that is your bottleneck.
Long Tasks (>50 ms) are marked with a red triangle in the timeline. They block the main thread and degrade INP and LCP.
Connection to Core Web Vitals
CRP directly affects all three metrics:
- LCP — this is the moment when the main visual element completes Layout + Paint. The faster the CRP, the better the LCP.
- CLS — if Layout is recalculated after the first Paint, elements can "jump," increasing CLS.
- INP — if a JS handler forces the browser to perform Layout/Paint across more than one frame, INP drops.
For more detail, see Core Web Vitals 2026.
Summary
The Critical Rendering Path is not a theoretical concept — it is a concrete chain of operations that can be accelerated step by step:
- Streaming + HTML compression
- Inline critical CSS + preload the rest
- Defer/async JS
- Minimize the DOM
- GPU-only animations (transform/opacity)
Understanding CRP lets you optimize with precision, rather than working through a checklist from PageSpeed Insights.
Want a detailed CRP analysis of your site? Get in touch. Free within 2 business days.