The critical path

Know exactly which tags delay the first paint, and order the head so they do not.

14of 18
≈ 13Min read
5Parts

The critical path is what has to arrive before anything is shown

The browser cannot paint until it has the HTML it has parsed so far and every stylesheet that applies to it. Anything else — images below the fold, analytics, the chat widget — can arrive later without the user noticing. The work of making a page feel fast is mostly deciding which is which.

the minimum for a first paint
HTML (enough of it)  +  every render-blocking stylesheet  →  first paint

Scripts: three behaviours

defer is the right default for application code: it downloads in parallel and runs in order once the DOM exists. async is for genuinely independent scripts — analytics — where "whenever" is fine and running before the page is built is harmless.

An inline script blocks the parser too, and it also blocks on any stylesheet above it, because it might ask for a computed style. An inline script between your CSS and your content is a stall nobody sees in the network panel.

TagBlocks the parserRuns
<script src>yes — parsing stops until it has runimmediately, in order
<script defer src>noafter parsing, in order, before DOMContentLoaded
<script async src>nowhenever it arrives — order not guaranteed
<script type="module">no (deferred by default)after parsing, in order

Stylesheets block painting, not parsing

Every blocking stylesheet is another round trip before anything appears. Splitting the rules the first screen needs out of the rest — or inlining that small part in the document — is the oldest trick in this lesson and still the most effective.

and a media query can take one off the critical path
<link rel="stylesheet" href="/app.css">                          <!-- blocks paint -->
<link rel="stylesheet" href="/print.css" media="print">          <!-- does not -->
<link rel="stylesheet" href="/wide.css" media="(min-width:60em)"><!-- only on wide screens -->

Telling the browser what it will need

the four hints, in the order you will use them
<link rel="preconnect" href="https://api.doors.test">              <!-- open the connection now -->
<link rel="preload" href="/fonts/space.woff2" as="font" crossorigin> <!-- fetch it at high priority -->
<link rel="prefetch" href="/next-page.js">                          <!-- for the NEXT navigation, idle -->
<link rel="modulepreload" href="/app.js">
preload everything and you have prioritised nothing

Preload raises a resource above others; three or four of them is a plan, twenty is a re-ordering with extra steps. The browser already prioritises well — hint only for things it cannot discover early, like a font referenced from a CSS file it has not parsed yet.

Fonts, and the invisible text

Without font-display, most browsers hide the text for up to three seconds waiting for the font. swap shows it at once in a fallback and swaps — a visible reflow, and far better than a blank page. optional is stricter still: use the font only if it arrives almost immediately, otherwise never for this visit.

one line that decides whether text appears
@font-face {
  font-family: "Space Grotesk";
  src: url(/fonts/space.woff2) format("woff2");
  font-display: swap;      /* show the fallback immediately, swap when it loads */
}

Practice