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.
Work in progress
This article is still being written and may change.
You type a URL and press Enter. A fraction of a second later, something appears. Between those two moments the browser runs a tightly ordered relay - fetch, parse, style, lay out, paint - and the slowest leg decides how long you stare at a blank screen.
That relay is the critical rendering path: the minimum set of work the browser must finish before it can show you anything. This article walks the whole path, one stage at a time, from the network round trip to the first painted pixel. Every stage has a live demo you can poke at.
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 everythingand they stare at white for too long. 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.
Before a byte renders: the network
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
Three levers move that bar, and they're the heart of HTML-level performance:
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.)
Move the server closer. A CDN caches your response on edge servers near the user, cutting round-trip time. It also brings HTTP/2, HTTP/3, and compression for free.
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=18And 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 - but 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.
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 nowThe 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. And there's a subtler dependency: because a script might read computed styles, the browser won't run it while a stylesheet is still downloading - so 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.
<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
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 every way 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. It combines them into a render tree (keeping only visible nodes - anything display: none is dropped entirely; visibility: hidden is different, it still gets a layout box, just an invisible one), computes layout (the exact geometry of every box), paints those boxes into layers of pixels, and finally composites the layers together 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.
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
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.
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.
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.