Critical Rendering Path
How browsers parse HTML, CSS, and JavaScript to produce the first frame — what blocks it, what the key metrics are, and how to optimize it.

Overview
The Critical Rendering Path (CRP) is the sequence of steps a browser must complete before it can paint the first pixel on screen. Every millisecond spent in this pipeline is time the user sees nothing — a blank or partially loaded page.
Understanding the CRP is the foundation of frontend performance work. Layout shifts, slow First Contentful Paint, and render-blocking resources all trace back to decisions made (or not made) about this pipeline. Optimizing it is one of the highest-leverage things you can do for perceived load performance.
This article covers the pipeline itself in detail. The next two articles — Render-Blocking Resources and Paint vs Layout vs Composite — go deeper on specific phases. The Core Web Vitals section covers how CRP optimization maps to measurable user-centric metrics.
How It Works
The browser follows a strict sequence from raw bytes to visible pixels. You can't skip steps — each one feeds the next.
Step 1 — Bytes to Characters to Tokens
The browser receives raw bytes over the network. It converts them to characters using the encoding specified in the HTML (usually UTF-8), then tokenizes the character stream into meaningful units: opening tags, closing tags, attribute names, attribute values, text content.
Step 2 — Tokens to DOM
Tokens are converted into nodes, and nodes are linked into the Document Object Model (DOM) — a tree structure representing every element, attribute, and text node in the document. The DOM is built incrementally as the parser reads the HTML byte stream top to bottom.
Bytes → Characters → Tokens → Nodes → DOMHTML parsing is incremental — the browser can begin building the DOM before the full document arrives. This is what makes streaming HTML valuable: the browser starts working the moment the first bytes land.
Step 3 — CSS to CSSOM
Every CSS source — external stylesheets, <style> blocks, inline styles — is parsed into the CSS Object Model (CSSOM). Unlike the DOM, the CSSOM is not built incrementally. CSS is cascade-dependent: a rule at the bottom of a stylesheet can override a rule at the top, so the browser must have the complete stylesheet before it can resolve any computed styles.
This is why CSS is render-blocking by default. The browser will not advance past step 4 until every CSS file it knows about has been downloaded and parsed.
Step 4 — DOM + CSSOM → Render Tree
The browser combines the DOM and CSSOM into the render tree — a new tree containing only the nodes that will actually be painted on screen. Nodes with display: none are excluded entirely. visibility: hidden nodes stay in the tree (they take up space) but paint nothing.
This step cannot begin until both the DOM and CSSOM are ready.
Step 5 — Layout (Reflow)
With the render tree built, the browser calculates the exact position and size of every node — a process called layout (or reflow). It starts at the root and works down the tree, computing the geometry of each element in relation to its parent and siblings.
Layout is expensive. Any change that affects the geometry of even one element can force the browser to recalculate the positions of many others.
Step 6 — Paint
The browser rasterizes each node — filling in pixels for background colors, borders, text, images, and shadows. Paint happens per-layer and is handled by the CPU by default.
Step 7 — Composite
The painted layers are combined by the GPU into the final frame and sent to the display. CSS properties that only affect compositing — transform, opacity — can be animated on the GPU without triggering layout or paint.
HTML bytes
↓
DOM
↓ ← CSS bytes → CSSOM
Render Tree
↓
Layout
↓
Paint
↓
Composite → screenWhere JavaScript Fits
When the HTML parser encounters a <script> tag without async or defer, it must:
- Pause HTML parsing entirely
- Wait for any pending CSS to finish downloading and parsing (because JS can query computed styles via
getComputedStyle) - Download the script
- Execute it
- Resume parsing
This makes synchronous scripts both parser-blocking and render-blocking — one of the most impactful things you can get wrong in a page's <head>.
Code Examples
Identifying CRP Issues with PerformanceResourceTiming
// Run in browser console or a monitoring script to surface render-blocking resources
const resources = performance.getEntriesByType(
"resource",
) as PerformanceResourceTiming[];
const blocking = resources.filter(
(r) => (r as any).renderBlockingStatus === "blocking",
);
console.table(
blocking.map((r) => ({
name: r.name.split("/").pop(),
duration: `${r.duration.toFixed(0)}ms`,
size: `${((r as any).encodedBodySize / 1024).toFixed(1)}kb`,
})),
);Script Loading Attributes Compared
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Product Page</title>
<!-- ✅ Critical CSS — render-blocking is acceptable here.
Keep this as small as possible (above-the-fold styles only). -->
<link rel="stylesheet" href="/styles/critical.css" />
<!-- ✅ Non-critical CSS deferred via the media trick.
Browser downloads at low priority, applies on load. -->
<link
rel="stylesheet"
href="/styles/below-fold.css"
media="print"
onload="this.media='all'"
/>
<!-- ✅ async: downloads in parallel, executes immediately when ready.
Does NOT preserve execution order.
Use for fully independent scripts (analytics, widgets). -->
<script async src="/scripts/analytics.js"></script>
<!-- ✅ defer: downloads in parallel, executes after full HTML parse.
Preserves execution order. Use for your application bundle. -->
<script defer src="/scripts/app.js"></script>
<!-- ✅ preload: high-priority fetch for resources discovered late,
like fonts embedded in CSS or hero images in background-image. -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
</head>
<body>
<h1>Featured Products</h1>
</body>
</html>Inlining Critical CSS in Next.js
Next.js automatically extracts and inlines critical CSS from CSS Modules. For global styles, you can manually inline the above-the-fold subset:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
{/*
Inline only the CSS needed to render the first viewport.
Target: ~1–5KB. Larger than this and the HTML payload cost
outweighs the round-trip savings on repeat paths.
*/}
<style
dangerouslySetInnerHTML={{
__html: `
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { display: flex; min-height: 60vh; align-items: center; }
.nav { height: 64px; border-bottom: 1px solid #e5e7eb; }
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}Measuring CRP Milestones with PerformanceNavigationTiming
// Log key CRP timing milestones after the page loads
window.addEventListener("load", () => {
const [nav] = performance.getEntriesByType(
"navigation",
) as PerformanceNavigationTiming[];
console.table({
"DNS lookup": `${(nav.domainLookupEnd - nav.domainLookupStart).toFixed(0)}ms`,
"TCP connect": `${(nav.connectEnd - nav.connectStart).toFixed(0)}ms`,
TTFB: `${(nav.responseStart - nav.requestStart).toFixed(0)}ms`,
"DOM interactive": `${nav.domInteractive.toFixed(0)}ms`,
"DOM content loaded": `${(nav.domContentLoadedEventEnd - nav.startTime).toFixed(0)}ms`,
"Load complete": `${(nav.loadEventEnd - nav.startTime).toFixed(0)}ms`,
});
});Real-World Use Case
An e-commerce product listing page has a slow FCP. The Performance tab in DevTools shows a 340ms gap before First Contentful Paint where the HTML parser is stalled. Drilling into the flame chart reveals a third-party A/B testing script in <head> with no async or defer attribute — the parser is waiting for it to download and execute before continuing.
Adding async (the script is fully independent of the DOM) drops the FCP by 340ms across every page load. One attribute change, measurable impact at scale.
Common Mistakes / Gotchas
1. Treating all render-blocking as equally bad. CSS that styles above-the-fold content should be render-blocking — you want it applied before the first paint to avoid a flash of unstyled content. The goal isn't to eliminate all render-blocking, it's to eliminate unnecessary render-blocking. Your primary stylesheet is not the problem. A third-party chat widget stylesheet is.
2. Adding defer to scripts without addressing CSS.
A deferred script won't block parsing — but if a large external stylesheet appears before it in the HTML, the browser still waits for that stylesheet before executing the script (because the script might query computed styles via getComputedStyle). The stylesheet remains a CRP bottleneck through an indirect dependency that defer alone doesn't fix.
3. Over-preloading resources.
rel="preload" competes with other high-priority fetches. Preloading a carousel image below the fold or a font used only in the footer can delay FCP by starving the actually critical resources of bandwidth. Only preload resources the browser won't discover quickly on its own and that are needed for the first paint.
4. Dismissing inlining because "the CSS file is cached." Browser caching helps on repeat visits. On a first visit — where CRP optimization matters most — the browser must make a full round-trip to fetch the stylesheet before it can paint. Inlining the above-the-fold subset eliminates that round-trip for first-time visitors, which is exactly when you most want to make a good impression.
5. Benchmarking only on fast connections. CRP bottlenecks are dramatically more visible on slow networks. A 100ms stylesheet download on fibre becomes 800ms on throttled 3G. Always test CRP optimizations under throttled conditions in DevTools — use the "Slow 3G" preset — to see the real-world impact before shipping.
Summary
The Critical Rendering Path is the browser's pipeline from bytes to pixels: HTML → DOM, CSS → CSSOM, both combined into the render tree, then layout, paint, and composite. No step can begin until its predecessor is complete, and any resource that stalls DOM or CSSOM construction delays the first paint. CSS is render-blocking by design. JavaScript is parser-blocking and render-blocking by default unless marked async or defer. The primary optimization levers are: eliminate unnecessary render-blocking resources, inline the CSS needed for the first viewport, preload late-discovered critical assets, and measure with FCP and LCP under realistic throttled conditions rather than fast lab environments.
Interview Questions
Q1. Walk me through the Critical Rendering Path from HTML bytes to pixels.
The browser receives raw bytes, decodes them to characters, tokenizes the character stream, and builds the DOM tree incrementally as it parses. Any CSS encountered is downloaded and parsed into the CSSOM — unlike the DOM, the CSSOM must be fully built before proceeding because styles cascade. The DOM and CSSOM are combined into the render tree, which contains only visible nodes with their computed styles. The browser then runs layout to calculate every element's geometry, paints pixel data per layer on the CPU, and composites the layers on the GPU into the final frame. Any resource that blocks DOM or CSSOM construction stalls this entire pipeline.
Q2. Why is CSS render-blocking, and can you make it non-blocking?
CSS is render-blocking because the render tree — which requires computed styles — cannot be built without a complete CSSOM. Rendering before CSS arrives would produce a Flash of Unstyled Content followed by a full re-render, which is a worse user experience than waiting. You can defer non-critical CSS by setting media="print" on the link element and switching it to media="all" in the onload handler — the browser treats print stylesheets as low priority and doesn't block rendering for them. This technique is appropriate for below-the-fold styles, but CSS that affects the first visible viewport should remain blocking.
Q3. What is the difference between async and defer on a script tag?
Both download the script in parallel with HTML parsing rather than blocking the parser during download. They differ in when execution happens. async executes the script as soon as it's downloaded, potentially interrupting HTML parsing, and does not preserve execution order between multiple async scripts. defer executes after HTML parsing is complete and preserves execution order. Use async for fully independent scripts like analytics or A/B testing tags. Use defer for application code that needs a complete DOM or depends on other scripts executing first.
Q4. Why does a deferred script sometimes still delay rendering when a large stylesheet is present?
JavaScript can query computed styles via getComputedStyle, so the browser must ensure the CSSOM is fully built before executing any script — even a deferred one. If a large external stylesheet appears in the HTML before a deferred script, the browser will download the stylesheet and build the CSSOM before running the script, regardless of the defer attribute. The stylesheet is still a CRP bottleneck through this indirect dependency chain. The fix is to make critical stylesheets as small as possible — not just to defer scripts.
Q5. What is rel="preload" and when should you use it vs rel="prefetch"?
rel="preload" tells the browser to fetch a resource at high priority as early as possible — before the parser would normally discover it. Use it for resources critical for the current page's first paint that the browser discovers late: fonts referenced in CSS, hero images in background-image, or scripts loaded dynamically. rel="prefetch" fetches a resource at low priority for likely future navigations — it doesn't help the current page. Overusing preload is a common mistake: every preloaded resource competes with other high-priority fetches, so preloading non-critical resources can actively hurt FCP.
Q6. How does the CRP directly connect to Core Web Vitals metrics?
First Contentful Paint (FCP) measures when the browser paints the first DOM content — it's a direct measure of how long the CRP takes to produce the first visible output. Largest Contentful Paint (LCP) measures when the largest content element in the viewport is painted — it extends the CRP story to include image loading and font rendering. Both are degraded by the same CRP bottlenecks: render-blocking CSS, parser-blocking scripts, and slow TTFB. Optimizing the CRP — removing unnecessary blocking resources, inlining critical CSS, preloading key assets — directly improves both FCP and LCP scores without any React or framework-specific changes.
Overview
How browsers turn HTML, CSS, and JavaScript into pixels — and how React's rendering model maps onto that pipeline.
Render-Blocking Resources
What makes a resource render-blocking, how async and defer change script execution, how to defer non-critical CSS, and how to measure and eliminate blocking resources in production.