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

Connect170msRedirect0msServer175msDownload304ms

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=18

And 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. 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>
<head>
<title>CRP</title>
</head>
<body>
<h1>Hi</h1>
<p>Welcome</p>
</body>
</html>
html
1 / 6 parsed

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

Blocking <link> in <head> (browser default)

Acme Storefront

The fastest way to check out online.

Shop now
HTML parsed
CSS ready
No flash — but the screen stays blank longer.

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 render

JavaScript 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.

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

<script>parsing done at 100
Main
Network

Parser stops dead at the tag, waits for download AND execution, then resumes.

<script async>parsing done at 74
Main
Network

Downloads in parallel, but executes the moment it arrives — pausing the parser whenever that is.

<script defer>parsing done at 60
Main
Network

Downloads in parallel, never blocks parsing, runs in order after the HTML is fully parsed.

Download JSExecute JSParse HTML
<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 order

The 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

Resources written in the HTML (scanner sees them)
app.js
styles.css
hero.jpg
Everything ready at0.99sCSS + image download alongside the script, not after it.

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), 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

body
header
h1
button
aside

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

connection
document.html
styles.css
print.css media="print"
app.js
hero.webp
First paint
render-blocking non-blocking JS execution

First paint

928ms

no optimizations applied

Preconnectopen the connection sooner
Brotli compressionfewer bytes to download
Defer the scriptstop JS blocking paint
media="print" on print.cssstop unused CSS blocking
Inline critical CSSremove the CSS round trip

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 (or async) to scripts so they never block the parser.
  • Inline critical CSS and mark non-critical stylesheets with a media attribute.
  • Keep critical resource URLs in the HTML so the preload scanner can find them.
  • Use preconnect / preload to 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.