The Critical Rendering Path - From URL to First Pixel
Follow one request from the moment you press Enter to the first painted pixel - with live demos at every step.
You type a URL and press Enter. A moment later, the page appears. In between, the browser runs a strict relay - fetch, parse, style, layout, paint - where each leg hands off to the next, and the slowest one decides how long you stare at a blank screen.
That relay is the critical rendering path: the smallest amount of work the browser has to finish before it can put anything on screen. This article follows it end to end, one stage at a time, from the first network round trip to the first painted pixel - with a live demo at every step.
The race to first pixel
A native app is installed once and then just runs. The web is different: every visit re-downloads the program (your HTML, CSS, and JavaScript) over a network you don't control, and the browser starts building the page before the download even finishes.
The browser is constantly making a trade-off. Paint too early and the user sees a broken, half-styled flash. Wait for everything and they sit too long with nothing to look at. The critical rendering path is the browser's answer: render as soon as the essential resources are ready, and defer the rest. Our job as engineers is to make that essential set as small and as fast as possible.
The whole path, before we walk it:
- Network - fetch the HTML document (this is TTFB).
- Parse - HTML becomes the DOM, as the bytes stream in.
- Style - CSS becomes the CSSOM, and blocks the first paint.
- Script - JavaScript runs, and can freeze the parser.
- Pixels - the DOM and CSSOM become a render tree, then layout, paint, and composite.
First paint is the finish line. We've grouped the path into five stages; we'll walk each one in order.
Before a byte renders
Nothing can render until the first byte of HTML arrives. That delay - Time to First Byte(TTFB) - is pure overhead, and it's built from a few stacked costs: opening the connection, the server thinking, and the bytes travelling back.
Time to first byte
Server
Compression
Redirect
First byte arrives at
649ms
HTML over the wire
92 KiB
Server-Timing: auth;dur=55, db;dur=120
Move the server to a CDN and switch on Brotli - every round trip and surplus byte you cut slides the bar left.
Three levers move that bar, and they're where most of your TTFB wins come from:
Cut redirects. Each redirect is a whole extra round trip before the real response even starts. A stray trailing-slash redirect can cost you 100–200ms for nothing - link straight to the final URL.
Compress text. HTML, CSS, JS, and SVG are highly compressible. Brotli beats gzipby roughly 15–20% and is supported everywhere, so fewer bytes cross the wire and TTFB's download leg shrinks. (Files under ~1 KiB aren't worth compressing.) If you want to dig into how Brotli works, MDN has a good primer.
Move the server closer. A CDN caches your response on edge servers near the user, cutting round-trip time.
For dynamic pages, measure where the server time actually goes with the Server-Timing response header - it surfaces backend phases right inside DevTools:
Server-Timing: auth;dur=55, db;dur=120, render;dur=18Each entry is a name;dur=millisecondspair, so here auth took 55 ms, the database 120 ms, and render 18 ms - the names are yours to choose. If you want the full syntax, MDN has a good Server-Timing reference.
One more win, on repeat visits: because HTML usually references fingerprinted assets - URLs with a content hash that changes on every deploy - it's safe to cache the HTML for a short window (a few minutes) and revalidate with ETag / If-None-Match. Just never cache personalized or authenticated HTML.
HTML becomes the DOM
The first byte has arrived. Now the browser parses HTML into the Document Object Model(DOM) - a tree of nodes representing your markup. It gets there in stages: raw bytes become characters (using the page's encoding, like UTF-8), those characters are grouped into tokens - <html>, <h1>, and the text between them - and each token becomes a node linked into the tree. Crucially, this happens as the bytes stream in. The browser doesn't wait for the whole file; it builds the tree token by token and can start rendering the top of the page while the bottom is still downloading.
HTML streams into the DOM
Step forward and each token becomes a DOM node the moment it's read - the browser never waits for the whole file.
This streaming model is why where you put things in the HTML matters so much. Anything that interrupts the parser - or arrives later than it could have - pushes back every node below it. The next three stages are all about those interruptions.
CSS blocks the first paint
CSS is render-blockingby default. The browser will keep parsing HTML, but it won't paint anything until it has built the CSS Object Model (CSSOM) from every stylesheet in the <head>. That sounds wasteful, but it's deliberate: painting before the styles are ready would flash unstyled content and then visibly reflow.
Render-blocking CSS
Acme Storefront
The fastest way to check out online.
Shop nowTurn blocking off and paint lands sooner - at the cost of the unstyled flash the browser is trying to spare you.
The escape hatch is to tell the browser a stylesheet doesn't apply right now. A media attribute that doesn't match the current conditions makes that sheet non-render-blocking - the classic example is print styles:
<link rel="stylesheet" href="app.css" /> // blocks render
<link rel="stylesheet" href="print.css" media="print" /> // does NOT block renderJavaScript and the parser
A plain <script> is parser-blocking. When the parser hits one, it must stop, download the file, run it to completion, and only then continue - because the script could call document.writeor otherwise change the DOM it hasn't built yet. A parser-blocking script is effectively render-blocking too. There's also a subtler dependency: a script might read computed styles, so the browser won't run it while a stylesheet is still downloading. Slow CSS can stall your JavaScript, which in turn stalls the parser.
Two attributes fix this. async downloads the script in parallel and runs it the instant it arrives (pausing the parser whenever that happens, in no guaranteed order). defer also downloads in parallel but waits until parsing is fully done, running scripts in order. For anything that touches the DOM, defer is almost always what you want.
async vs defer
Parser stops dead at the tag, waits for download AND execution, then resumes.
Downloads in parallel, but executes the moment it arrives - pausing the parser whenever that is.
Downloads in parallel, never blocks parsing, runs in order after the HTML is fully parsed.
Play it back to see where each script runs: plain blocks the parser, async cuts in whenever it lands, defer waits for the end.
<script src="a.js"></script> // blocks the parser
<script src="b.js" async></script> // runs whenever it lands
<script src="c.js" defer></script> // runs after parsing, in orderThe preload scanner
If a blocking script froze the main parser, how does the browser ever stay fast? A second, lightweight parser - the preload scanner - races ahead through the raw HTML while the main parser is stuck, spotting <link>, <img>, and <script> URLs and kicking off their downloads early, in parallel.
It only works on resources actually written in the HTML. The moment you injecta resource with JavaScript, the scanner can't see it - discovery waits until the script runs, and the download starts far too late:
The preload scanner
Inject the resources with JavaScript and discovery slips late - the scanner only sees what's written in the HTML.
The lesson: keep your critical references in the markup. Reach for <link rel=preload> and <link rel=preconnect>to nudge discovery even earlier, and avoid loading important CSS or fonts through a JS bundle. For all the ways you can accidentally blind it, Jeremy Wagner's Don't fight the browser preload scanner is the definitive guide.
Turning trees into pixels
With the DOM and CSSOM ready, the browser runs the final pipeline in four steps. First it builds a render tree - the DOM and CSSOM merged down to just the visible nodes. Anything set to display: none is dropped entirely; visibility: hidden is the subtle case - it still gets a box, just an invisible one. Then layout computes the exact geometry of every box, paint fills those boxes with pixels, and composite stacks the layers into the frame you see.
From trees to pixels
DOM
CSSOM
h1 { font-size: 28 }
button { bg: accent }
aside { display: none }
DOM + CSSOM. Two trees: the structure (DOM) and the styles (CSSOM), built separately.
Step through render tree, layout, paint, and composite to watch a single box turn into pixels.
Understanding these stages pays off later, too: a CSS change that only affects color skips straight to paint, while one that changes size forces a full layout - which is why some animations are cheap and others jank.
The critical path, assembled
Put it together and the critical resources - the ones that gate the first paint - are just three things: the HTML document, render-blocking CSS in the <head>, and parser-blocking JavaScript in the <head>. First paint happens when the last of those is ready.
So every optimization in this article does one of two things: it makes a critical resource arrive sooner, or it removes a resource from the critical set entirely. Toggle them and watch the first-paint line slide left:
Move the first-paint line
First paint
928ms
no optimizations applied
Toggle each optimization and the first-paint line slides left as resources drop out of the critical set.
Modern metrics push this further. First Contentful Paint and Largest Contentful Paint care about meaningful content, not just any first paint - so the practical goal is the shortest path to the content the user actually came for.
You can watch the whole path in your browser's DevTools: the Network panel's waterfall shows each critical resource in flight, and two events mark the milestones - DOMContentLoaded fires once the DOM and CSSOM are ready, and load once everything else (images included) has arrived.
The checklist
The whole path, distilled into things you can do today:
- Serve over a CDN with Brotli compression and as few redirects as possible.
- Add
defer(orasync) to scripts so they never block the parser. - Inline critical CSS and mark non-critical stylesheets with a
mediaattribute. - Keep critical resource URLs in the HTML so the preload scanner can find them.
- Use
preconnect/preloadto start key requests earlier. - Measure with Lighthouse and WebPageTest - they flag the resources that actually delay your render.
Want to go deeper? Google's Critical Rendering Path series on web.dev is the canonical reference - it walks DOM and CSSOM construction, the render tree, and how to measure and optimize the path in full detail.
Minimize the critical path and the gap between “press Enter” and “first pixel” collapses. That gap is the first thing every visitor feels.
Newsletter
Stay updated with my latest articles and projects. No spam, no nonsense.