Web Platform · Rendering Performance

Render-Blocking CSS and Parser-Blocking Scripts on the Critical Path

Why a stylesheet stops the first paint and why a script stops the parser, which dependencies the preload scanner can find on its own, and what defer and async actually order.

The document parser is a single pass over a byte stream that produces DOM nodes as it goes. Two resource types can interrupt that pass: a stylesheet, because the browser refuses to paint content whose style it does not know, and a classic script, because a script is allowed to modify the document that the parser is still constructing. Both interruptions were designed to make documents deterministic. Both become the dominant cost of first paint on a slow connection.

The parser stops at a classic script, and the stop is transitive

A <script> element with src and no async, defer, or type="module" is a classic, parser-inserted script. When the parser reaches it, the following happens in order:

  1. The parser stops tokenizing.
  2. If any stylesheets are still loading, the script waits for them, because the script may read computed styles and the result must be stable.
  3. The script is fetched if not already fetched, then executed on the main thread.
  4. The parser resumes from the byte offset where it stopped.

The transitive part is step 2. A single stylesheet in the head delays every later script, and therefore delays every later byte of HTML from becoming DOM. This is why a page with a slow stylesheet and one synchronous script behaves as if it had one blocking resource with the latency of both in series, not in parallel.

Because parsing has stopped, markup after the script is invisible to the parser and, for that window, to the engine’s speculative scanner. Any image, font, or subresource declared below a synchronous script has its discovery delayed by the script’s fetch and execution time.

Why CSS blocks paint even when nothing visible uses it

A stylesheet blocks rendering because the engine cannot paint a partial style: content painted with the wrong styles would be visible for a frame and then change, which is a flash of unstyled content. The rule is applied per document, not per stylesheet, with two refinements that matter.

First, a link whose media attribute does not match the current environment is still fetched, but it is not render-blocking; the browser can paint without it. The query is evaluated without waiting for the sheet, so separating a print stylesheet with media="print" keeps it off the critical path. Second, an @import inside a stylesheet is discovered only after the importing sheet has been fetched and parsed. A chain of imports serialises round trips: each hop costs a full request before the next URL is known. Flattening imports into one file converts that chain into one request.

rel="preload" does not remove blocking; it only starts the fetch earlier. Marking a stylesheet as preloaded as well as linked lets the fetch begin when the preload scanner sees it, but paint still waits for the bytes and for parsing. Removing a stylesheet from the critical path requires either inlining the rules that the first viewport needs or making the sheet non-blocking, which trades a blocked paint for a repaint when the sheet arrives.

The preload scanner makes the fetch independent of the parser

The speculative scanner is a second, lightweight parser that reads ahead of the main parser for subresource URLs: <img src>, <script src>, <link href>, srcset, and the like. It starts fetches without building DOM, so a resource can be in flight before the main parser reaches it. This is why resource order in the HTML often matters less than people expect: an image at the bottom of a long document can begin loading early.

Its blind spots are the reason some resources are discovered late:

  • URLs constructed by script are invisible until the script runs.
  • Markup injected by document.write requires the parser to be paused and restarted, and the scanner cannot know what that markup will contain. Content produced this way is effectively outside speculative discovery.
  • A classic script is a barrier the scanner must treat conservatively, since the script may rewrite the document.
  • @import chains are discovered only as each sheet parses, unless the nested URL appears in the importing file.

The practical corollary is that late discovery, not slow transfer, is often what puts a resource after first paint. A font referenced only from CSS is discovered after the CSS is parsed; a font preloaded in the head is discovered by the scanner at once.

Ordering guarantees for defer, async, and modules

The three non-blocking script forms differ in two properties: whether they block parsing, and what order execution happens in.

FormBlocks parsingExecution orderRelative to DOMContentLoaded
classic srcyesdocument orderbefore
defernodocument order, after parsingbefore
asyncnocompletion ordermay be after
type="module"nodocument order, after parsingbefore
type="module" asyncnocompletion ordermay be after

defer scripts execute after the parser finishes but before DOMContentLoaded fires, in document order. That makes them safe for code that needs the whole DOM and needs its dependencies in a fixed order. async scripts execute whenever they arrive, so two async scripts with a dependency between them are a race. Module scripts are deferred by default and preserve import order; adding async removes both guarantees.

async is not a way to make a script harmless. It removes the parser block, but execution still happens on the main thread and still competes with style and layout for the frame. A large async script that runs during the first frame delays paint just as surely as a blocking one, while paying the additional cost of executing at an unpredictable moment.

How the first paint gets decoupled from the network

The techniques that shorten the path all do one of three things: reduce the bytes needed before the first paint, move discovery earlier, or remove a serial dependency.

Inlining the rules for the initial viewport into the document avoids a request entirely; the remainder can be loaded in a way that does not block the first paint. Preloading the few subresources that are certain to be needed (a hero image, a font used above the fold) starts their fetches at scanner time. Caching makes the second visit’s blocking resources free of network latency, which is where HTTP cache validators earn their keep: a conditional request that returns a not-modified response still costs a round trip, while a fresh cache entry costs none.

<link rel="preload" as="style" href="/app.css">
<link rel="stylesheet" href="/app.css" media="screen">
<link rel="stylesheet" href="/print.css" media="print">
<style>/* first-viewport rules */</style>
<script src="/app.js" defer></script>

The order above does not change the paint dependency on /app.css; it only moves the fetch to scanner time. The media="print" sheet is downloaded without holding the paint, and the deferred script runs after parsing without blocking it.

A dependency that is easy to miss is the font. Text is painted with a fallback face while the web font loads, then re-laid out when the font arrives, which can move every line of text on the page. That relayout is a layout cost paid after first paint, sometimes after the user has started reading, and it is the same mechanism described in Layout Invalidation and Dirty Bits. Sizing the fallback to match the web font, or limiting the faces needed for the first screen, reduces the work without removing the behaviour.

Reading the waterfall as a dependency chain

The network panel shows requests with start times and durations, but the informative quantity is the chain: first byte of the HTML, discovery of the stylesheet, download of the stylesheet, parse of the stylesheet, discovery of the script that was waiting behind it, download of the script. Each link in that chain adds at least one round trip, and on a connection with a round-trip time in the hundreds of milliseconds the chain dominates every other cost. A page that loads six resources in parallel over one connection is bound by the slowest, not by the sum; a page that loads six resources in a chain is bound by the sum, and no amount of bandwidth fixes it.

Paint timing entries give the observable end of that chain. The paint entry type reports first-paint and first-contentful-paint with a startTime in milliseconds relative to navigation start, which is the number to compare against the chain rather than against the total load time. If first contentful paint occurs long after the HTML response completes, the delay is in a render-blocking dependency rather than in the document’s transfer.

What the path does not cover

The critical path described here ends at the first paint of content, not at a usable page. A page can paint its first frame and remain unresponsive while scripts hydrate, which is a scheduling problem rather than a fetching one, covered in Frame Budgets, Long Tasks, and Input Latency. The two costs are additive on slow devices: shortening the path moves the first paint earlier, but the main thread is still occupied immediately afterwards by whatever the deferred scripts do.

References

  1. HTML Standard — Scripting — WHATWG
  2. CSS Object Model (CSSOM) — W3C
  3. HTML Standard — Link types — WHATWG
  4. RFC 9111: HTTP Caching — IETF