The Critical Rendering Path Explained
Open a web page and the browser immediately starts a race against the user's attention. Every millisecond it spends constructing the page before painting anything visible is a millisecond the user sees a blank screen. The sequence of steps between receiving bytes and displaying pixels is called the critical rendering path (CRP), and shortening it is the most direct lever you have on perceived load performance.
The Five Steps
1. Parse HTML → Build the DOM
The browser receives HTML bytes, decodes them into characters, tokenises them, and builds the Document Object Model (DOM) — a tree where each node represents an element, attribute, or piece of text. This process is incremental: the browser doesn't wait for the whole document before starting.
2. Parse CSS → Build the CSSOM
While parsing HTML, the browser encounters <link rel="stylesheet"> tags. It fetches those files and builds the CSS Object Model (CSSOM) — a separate tree representing all styles. Unlike the DOM, the CSSOM is not incremental: the browser blocks rendering until the entire stylesheet is processed, because a later rule can override an earlier one.
This is why CSS is described as render-blocking. Any stylesheet in the <head> delays the first paint.
3. Combine → Build the Render Tree
The DOM and CSSOM are merged into the render tree. Only visible nodes make it in — elements with display: none are excluded entirely. Each node in the render tree carries its computed styles.
4. Layout (Reflow)
The browser walks the render tree and calculates the exact position and size of every element. This is called layout (or reflow). It accounts for the viewport size, box model, relative and absolute positioning, flexbox rules, and anything else that affects geometry. Layout produces a box model for each node.
5. Paint
Finally, the browser converts the layout into pixels on screen. This involves painting text, colours, images, borders, shadows. Modern browsers split this into separate layers and composite them on the GPU — but the first paint can't happen until steps 1–4 are at least partially complete for the visible portion of the page.
What Blocks Rendering
Two things can stall the critical rendering path:
CSS is render-blocking by default. The browser won't paint until all CSS in the <head> is downloaded and parsed. This is intentional — rendering without styles produces a flash of unstyled content (FOUC). The fix is to deliver critical CSS fast (inline it, reduce its size, split it) and defer non-critical CSS.
JavaScript is both parser-blocking and render-blocking. When the parser hits a <script> tag, it stops parsing HTML, fetches the script, executes it, and only then resumes. JS can query and mutate the DOM and CSSOM, so the browser can't safely proceed without running it first.
<!-- ❌ Blocks parser while fetching + executing -->
<script src="app.js"></script>
<!-- ✅ Fetches in parallel, executes after parse -->
<script src="app.js" defer></script>
<!-- ✅ Fetches in parallel, executes immediately when ready -->
<script src="app.js" async></script>
defer preserves execution order and runs after HTML is parsed. async runs as soon as the script is available — useful for independent scripts (analytics) but dangerous for scripts that depend on the DOM or each other.
Measuring the Critical Rendering Path
The key metrics are:
- First Contentful Paint (FCP) — when the browser renders the first bit of content (text, image, SVG).
- Largest Contentful Paint (LCP) — when the largest visible element finishes rendering.
- Time to First Byte (TTFB) — how long until the first byte of the HTML response arrives.
In the browser's Performance panel, look for the "Parse HTML", "Evaluate Script", "Recalculate Style", and "Layout" tasks on the main thread. Long tasks in the early timeline are CRP bottlenecks.
Practical Optimisations
Inline critical CSS. Extract the styles needed to render above-the-fold content and place them in a <style> tag in the <head>. Load the full stylesheet with a non-blocking technique:
<style>/* critical styles here */</style>
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
Reduce CSS payload. Remove unused rules (PurgeCSS, Tailwind's JIT), split stylesheets by route, and compress with Brotli or gzip.
Defer non-critical JS. Add defer to all scripts that don't need to run before the page renders. Avoid document.write() which forces a synchronous layout.
Preload key resources. Use <link rel="preload"> to tell the browser early about fonts, hero images, and above-the-fold resources:
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
<link rel="preload" href="/hero.jpg" as="image">
Minimise render-blocking third parties. Tag analytics and ad scripts with async or load them after the load event. A single slow third-party request can push your LCP by hundreds of milliseconds.
Use HTTP/2 or HTTP/3. Multiple parallel requests over a single connection reduce the latency cost of fetching multiple CSS and JS files.
The Fold Is a Moving Target
"Above the fold" depends on the device and viewport. Don't hardcode assumptions. Use lab tools (Lighthouse, WebPageTest) alongside real-user monitoring to understand what your actual users see first. Optimise for the most common viewport in your traffic, not a desktop assumption.
Conclusion
The critical rendering path is the browser's work queue before the user sees anything. CSS blocks rendering until it's fully parsed; JavaScript blocks the parser until it's executed. Shortening the path means delivering less, delivering it earlier, and deferring everything that isn't immediately needed. Those three levers — size, order, priority — are the basis of every meaningful load performance improvement.
