Skip to the content.

The critical rendering path is the story of your first paint

The critical rendering path is the story of your first paint

Between the moment the HTML arrives and the moment the first pixel appears, the browser runs a fixed sequence of steps: parse the HTML into a DOM, parse the CSS into a CSSOM, combine them into a render tree, lay it out, and paint. That sequence is the critical rendering path, and the reason it is worth knowing is that two things you control — CSS and synchronous JavaScript — can stall it. A page paints sooner not by doing the steps faster but by removing what blocks them, and to remove a blocker you have to know where it sits on the path.

The critical rendering path from HTML to first paint HTML becomes the DOM and CSS becomes the CSSOM; they combine into the render tree, which is laid out and painted. CSS blocks render and a synchronous script blocks parsing. A dot travels the path to the paint. DOM CSSOM render tree layout paint CSS blocks render a sync <script> blocks DOM parsing
The render tree needs both the DOM and the CSSOM, so CSS blocks the first paint; a synchronous script blocks the DOM it might rewrite.

CSS is render-blocking by design

The render tree needs the CSSOM, and the CSSOM is not ready until the browser has downloaded and parsed every stylesheet it has seen. So a single large <link> in the <head> holds the first paint hostage — the browser will not show a half-styled page. The fix is to ship the styles the first screen needs inline and defer the rest, or to mark a non-critical stylesheet as not blocking:

<!-- critical styles inline: no round-trip before first paint -->
<style>/* just what the above-the-fold layout needs */</style>

<!-- the rest loads without blocking render, then applies -->
<link rel="stylesheet" href="/full.css" media="print" onload="this.media='all'">

A synchronous script blocks parsing

When the parser hits a plain <script src>, it stops — it must fetch and run that script before continuing, because the script might call document.write and change the DOM it is building. Put that script in the <head> and you have paused DOM construction before the body even exists. The two attributes that fix this tell the browser the script does not need to block:

<!-- defer: fetch in parallel, run in order AFTER the DOM is parsed -->
<script src="/app.js" defer></script>

<!-- async: fetch in parallel, run as soon as it lands (order not guaranteed) -->
<script src="/analytics.js" async></script>

Use defer for your application code (it needs the DOM and it needs to run in order); use async for independent scripts like analytics that touch nothing else. The one thing you almost never want is a bare, synchronous script in the <head>.

The path is the mental model for every “why is it slow?”

Most first-paint problems reduce to one of these two blockers plus a third: a render-tree element that has to wait for a resource, like a web font or the LCP image. Once you see the page as this pipeline, the tactics fall out — inline critical CSS, defer scripts, preload the hero image, subset the font — because each one removes a specific stall from a specific step. The render-strategy exercise makes you choose where a page should render given its path, which is the same reasoning applied one level up. Optimising a first paint is not folklore; it is reading this diagram and asking, at each arrow, “what is the browser waiting for here, and can I stop making it wait?”