---
title: 'Critical Rendering Path'
source: 'https://academia.sh/en/courses/frontend-quality/critical-rendering-path'
course: 'Frontend Quality'
language: en
updated: '2026-08-17T18:11:05+00:00'
license: 'CC BY-SA 4.0'
---

# Critical Rendering Path

The resources first paint has to wait for; the distinction between render-blocking CSS and parser-blocking scripts, computing the round-trip count, and the markup patterns that remove blocking.

The previous lesson defined the metrics: if largest contentful paint happens late, there
is a problem. The delay can have three sources — the server producing the response, the
response being transported, and the resources the browser has to wait for before it can
paint anything. The third is entirely under the developer's control.

This lesson defines that wait. The critical rendering path is the chain of work that
must complete before first paint can happen. The chain's length can be measured and
shortened.

## Two Different Kinds of Blocking

The browser parses the document top to bottom and requests the resources it encounters
along the way. Two kinds of resource stop this flow, and they stop it differently.

**Render-blocking CSS.** Until an external stylesheet gets downloaded and resolved, the
browser paints nothing. The reason is the cascade: by the rule the Visual Presentation
with CSS course establishes, an element's computed value can change because of a
declaration in a file that has not arrived yet. Painting with incomplete style would mean
a page that looks completely different seconds later. So the browser waits. Parsing the
document continues — a stylesheet does not stop parsing — but painting stops.

**Parser-blocking script.** A script written directly into the document, carrying
neither `defer` nor `async`, stops parsing until it has downloaded and run. The reason is
that the script can modify the document: it can produce or delete the content that
follows it, so the browser cannot continue. Because parsing stops, nothing after it can
get discovered; painting cannot happen either.

The two kinds of blocking compound. A stylesheet that comes before a script also delays
the script's execution: because the script might read computed values, the browser has
to wait for the stylesheet to resolve. A single ordering decision thereby ties three jobs
together.

## Three Variables

The wait's length depends on three separate quantities, and separating them determines
which optimization will actually help.

**Byte count** is the part that depends on bandwidth. Compression and dead-code
elimination affect this.

**Round-trip count** is the part that depends on latency. When data gets sent over a
connection, the congestion window starts small and grows with each round trip; a large
file therefore does not arrive in a single round trip. This behavior, defined in the
Network Models and Protocols course, produces the following performance consequence: the
first bytes are cheap, the later ones are expensive.

**Chain depth**, in turn, depends on discovery order. If a resource's existence only
becomes known after another resource has downloaded, the two cannot be requested in
parallel; their durations add up.

The following model computes all three together.

```js
// critical-path.mjs — a computation modeling how many round trips first paint costs

const MSS = 1460;          // bytes: the payload one segment carries
const INITIAL_WINDOW = 10; // segments: how many segments can be sent in the first round

// The connection carries the congestion window across rounds; the window doubles each round.
function connection() {
  let window = INITIAL_WINDOW;
  return {
    get window() { return window; },
    // One stage: resources requested concurrently share the same window.
    stage(bytes) {
      let remaining = bytes.reduce((a, b) => a + b, 0);
      let round = 0;
      do {
        remaining -= window * MSS;
        window *= 2;
        round += 1;
      } while (remaining > 0);
      return round;
    },
  };
}

function firstPaint(scenario) {
  const b = connection();
  let round = scenario.connectionSetup;
  const breakdown = [["connection setup", scenario.connectionSetup, "-"]];
  for (const stage of scenario.stages) {
    const previousWindow = b.window;
    const t = b.stage(stage.bytes);
    round += t;
    breakdown.push([stage.name, t, `${previousWindow} -> ${b.window} segments`]);
  }
  return { round, breakdown };
}

// North Slope Measurement Station — the station list page.
// Connection setup: name resolution 1 + transport handshake 1 + secure handshake 1 = 3 rounds.
const scenarios = [
  {
    name: "A: blocking style and script up front",
    connectionSetup: 3,
    stages: [
      { name: "HTML body", bytes: [18_400] },
      { name: "blocking style + script", bytes: [62_000, 141_000] },
      { name: "style discovered by the script", bytes: [24_000] },
    ],
  },
  {
    name: "B: script deferred",
    connectionSetup: 3,
    stages: [
      { name: "HTML body", bytes: [18_400] },
      { name: "blocking style", bytes: [62_000] },
    ],
  },
  {
    name: "C: critical style inlined",
    connectionSetup: 3,
    stages: [
      { name: "HTML body (+ inlined style)", bytes: [18_400 + 9_800] },
    ],
  },
];

console.log("--- slow start: total bytes sendable by the end of round k ---");
let accumulated = 0;
let window = INITIAL_WINDOW;
for (let k = 1; k <= 4; k++) {
  accumulated += window * MSS;
  console.log(`round ${k}  window ${String(window).padStart(3)} segments  total ${String(accumulated).padStart(7)} bytes`);
  window *= 2;
}

console.log("\n--- round-trip breakdown to first paint ---");
const results = [];
for (const s of scenarios) {
  const r = firstPaint(s);
  results.push({ name: s.name, round: r.round });
  console.log(`\n${s.name}`);
  for (const [name, t, p] of r.breakdown) {
    console.log(`  ${name.padEnd(30)} ${t} rounds   ${p}`);
  }
  console.log(`  ${"TOTAL".padEnd(30)} ${r.round} rounds`);
}

console.log("\n--- comparing the scenarios ---");
const baseline = results[0].round;
for (const s of results) {
  console.log(`${s.name.padEnd(34)} ${String(s.round).padStart(2)} rounds   ratio to A: ${(s.round / baseline).toFixed(2)}`);
}
```

```
--- slow start: total bytes sendable by the end of round k ---
round 1  window  10 segments  total   14600 bytes
round 2  window  20 segments  total   43800 bytes
round 3  window  40 segments  total  102200 bytes
round 4  window  80 segments  total  219000 bytes

--- round-trip breakdown to first paint ---

A: blocking style and script up front
  connection setup               3 rounds   -
  HTML body                      2 rounds   10 -> 40 segments
  blocking style + script        3 rounds   40 -> 320 segments
  style discovered by the script 1 rounds   320 -> 640 segments
  TOTAL                          9 rounds

B: script deferred
  connection setup               3 rounds   -
  HTML body                      2 rounds   10 -> 40 segments
  blocking style                 2 rounds   40 -> 160 segments
  TOTAL                          7 rounds

C: critical style inlined
  connection setup               3 rounds   -
  HTML body (+ inlined style)    2 rounds   10 -> 40 segments
  TOTAL                          5 rounds

--- comparing the scenarios ---
A: blocking style and script up front  9 rounds   ratio to A: 1.00
B: script deferred                  7 rounds   ratio to A: 0.78
C: critical style inlined           5 rounds   ratio to A: 0.56
```

The first table shows the congestion window growing: in the first round a little over
fourteen kilobytes of data can be sent, and by the end of the fourth round that exceeds
two hundred kilobytes. This number is not a limit but a starting assumption; the initial
window depends on configuration, and the model exposes the value as a constant up front.

The three scenarios build the same page with three different markups. In scenario A, the
blocking style and script sit at the top of the document, and a second stylesheet gets
discovered only after the script has downloaded. In scenario B, the script gets deferred;
both its own bytes and the chain it discovers drop out of the critical path. In scenario
C, the first screen's style gets inlined into the document and no external request
remains.

The result reads through ratios: scenario B waits seventy-eight percent of A's time,
scenario C fifty-six percent. An absolute duration cannot be given, because a round's
duration depends on the network — but the round count follows from the markup itself and
is comparable.

The model simplifies reality at two points. It assumes that concurrent requests within a
stage share the same window, and it does not account for the window shrinking after idle
time. Both push the result in favor of A, not B and C; the real difference, in other
words, is not smaller than what the model shows but larger.

## Removing the Blocking

The ways of shortening the critical path correspond to the two kinds of blocking.

**Taking the script out of the critical path.** The `defer` attribute lets the script
download in parallel with parsing and run, in write order, only after document parsing
finishes. `async` also downloads in parallel but runs the moment it arrives; there is no
order guarantee, and running it still interrupts parsing at that moment. `defer` suits
scripts that depend on each other; `async` suits scripts that are independent and
self-contained. Scripts loaded as modules behave as deferred by default.

**Making a stylesheet conditional.** When the `media` attribute gets written on a
stylesheet link, the file does not block painting if the condition does not currently
hold. Print stylesheets and styles that only apply on wide screens get taken out of the
critical path this way. The file still downloads, but at low priority and without
holding up painting.

**Inlining critical CSS.** The declarations sufficient to paint the first screen get
written directly into the document; the remaining style loads through a non-blocking
path. This is the source of scenario C's gain, and it has a cost: inlined style cannot be
cached separately, arrives again with every page request, and enlarges the document. The
decision depends on the repeat-visit rate — a gain on a page dominated by first visits is
a loss on a panel opened every day.

**Moving a late-discovered resource earlier.** If a resource only gets discovered after a
stylesheet resolves or a script runs, it can be requested at the top of the document with
a `preload` declaration. This declaration is a correction, not an optimization: it should
not get used when a direct way to shorten the chain exists, because written incorrectly
it makes a resource download twice or takes bandwidth away from a resource that is
genuinely critical.

## The Discovery Chain

Scenario A's fourth line — "style discovered by the script" — alone produces more than
one round's cost, because that round can only start after the script has downloaded and
run.

The browser's preload scanner skims the document quickly before parsing it and requests
the resources it sees ahead of time. The one thing the preload scanner cannot see is
resources not written in the document: links inserted by a script, other stylesheets
called from within a stylesheet, addresses determined only after a configuration file
gets read.

This is why the most effective step in critical-path optimization is often not reducing
bytes but **flattening the chain**: making every critical resource visible in the
document's first bytes.

## The Station List Page's Document Head

```html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>North Slope Measurement Station — Stations</title>

  <!-- first screen's style: inlined, no external request -->
  <style>
    .station-list { display: grid; gap: 12px; }
    .station-list > li { min-height: 72px; }
  </style>

  <!-- remaining style: does not block painting, applies once it has downloaded -->
  <link rel="stylesheet" href="/assets/station.9f2c1a.css" media="print"
        onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/assets/station.9f2c1a.css"></noscript>

  <!-- print style never blocks painting -->
  <link rel="stylesheet" href="/assets/print.3ce8b0.css" media="print">

  <!-- application script: downloads in parallel with parsing, runs in order once parsing finishes -->
  <script src="/assets/entry.4a1c.js" defer></script>
</head>
```

The second link is a markup trick and needs explaining: a stylesheet written with
`media="print"` does not block painting, and once the file has downloaded, the `onload`
handler sets the media to `all`, applying the style. The second link inside `noscript`
makes sure the style still arrives when the script does not run. The trick exists because
there is no direct "blocking" declaration available for a stylesheet link.

The `min-height` declaration in the inlined style is for stability, not performance: the
list rows take their height before the data arrives, so the layout shift computed in the
previous lesson does not happen. Two goals meet in the same declaration.

## Summary

- The critical rendering path is the chain of work first paint has to wait for;
  render-blocking CSS stops painting, and a parser-blocking script stops both parsing
  and painting.
- The wait's three components get handled separately: byte count depends on bandwidth,
  round-trip count on latency, and chain depth on discovery order.
- Because the congestion window grows with rounds, the first bytes are cheap; taking a
  resource out of the critical path can give a much larger gain than shrinking it.
- `defer`, `async`, and `media` declarations remove blocking; inlining critical style
  erases the external request entirely but gives up cacheability.
- The preload scanner sees only resources written in the document; flattening the chain
  is often more effective than reducing bytes.

## Next Step

When the critical path shortens, first paint moves earlier, but on most pages largest
contentful paint is not a block of text — it is an image. The card images on the station
list and the station photo on the measurement detail page carry far more bytes than the
document itself; font files, in turn, determine when the text becomes readable. Both have
their own decisions about size, format, and loading. The next lesson takes up these
decisions and computes which candidate file downloads under which condition.
