Document Parsing
Date: 2026-08-16
The browser builds the tree as bytes arrive, and a plain
<script>stops it dead. Where your scripts sit and which attribute they carry is the difference between a page that paints early and one that waits.
What it is
Document parsing is the process turning the HTML byte stream into the DOM tree: bytes → characters → tokens → nodes → tree.
It’s incremental — the browser starts building and can start rendering before the document has finished downloading. That’s why a slow page still shows its header quickly, and why anything that interrupts the parser is expensive.
What stops the parser
<html>
<head>
<link rel="stylesheet" href="app.css">
<script src="analytics.js"></script>
</head>
<body>
<h1>Boots</h1>
...app.css blocks RENDERING, not parsing
analytics.js blocks PARSING — everything
stops here
<h1>Boots</h1> not even discovered until
the script above has run
A classic <script src> blocks because the script could call document.write() and change the markup that follows. The browser has no choice but to fetch it, execute it, and only then continue.
Stylesheets don’t block parsing but they do block rendering — the browser won’t paint before it knows the styles, to avoid showing unstyled content. Both delay first paint; they delay different stages. See The Critical Rendering Path.
defer and async
The two attributes that unblock, and they are not interchangeable.
<script src="a.js"></script> <!-- blocks parsing, runs immediately -->
<script src="b.js" async></script> <!-- parses on, runs whenever it lands -->
<script src="c.js" defer></script> <!-- parses on, runs after parse, in order -->
<script type="module" src="d.js"></script> <!-- deferred by default -->| Blocks parsing | Execution order | Runs when | |
|---|---|---|---|
| plain | Yes | Document order | Immediately on fetch |
async | No | Whichever downloads first | As soon as it arrives |
defer | No | Document order, guaranteed | After parsing, before DOMContentLoaded |
type="module" | No | Document order | Like defer |
async is for scripts with no dependencies and nothing depending on them — an independent analytics beacon. Using it for anything ordered produces a race that passes locally and fails on slow connections, because order depends on download timing.
defer is the sensible default for your own scripts. Parse continues, order is preserved, execution happens once the tree exists.
Both attributes are ignored on inline scripts, which always block.
The preload scanner
While the main parser is blocked, a secondary scanner runs ahead through the raw bytes looking for URLs and starts fetching them early. It’s why a blocking script hurts less than it should.
The consequence worth knowing: it only sees markup. Resources injected by JavaScript — a tag manager loading a font, a script inserting an image — are invisible to it and start late. That’s a large part of why third-party tags cost more than their file size suggests. See Third-Party Scripts and Resource Hints.
The events
parsing complete, deferred scripts run → DOMContentLoaded
images, stylesheets, subframes finished → load
DOMContentLoaded is when the tree is ready. load waits for every subresource and can be seconds later on an image-heavy page — which is why binding measurement or initialisation to load makes it fire long after the user has started interacting.
Error recovery
HTML parsing never fails. There is no such thing as invalid HTML that stops the browser — the specification defines a recovery behaviour for every malformed case, and every browser implements the same one.
<p>One
<p>Two → two sibling <p> elements. The first was auto-closed.
<table><div>x</div></table>
→ the div is moved OUT of the table, before it.
"Foster parenting". Surprises everyone once.This is why a missing closing tag produces a strange layout rather than an error, and why the tree can differ from your markup in ways that look like a CSS bug — see The DOM.
Practical
deferon everything of your own;asynconly for genuinely independent scripts- Nothing render-blocking in
<head>that isn’t critical CSS - Inline the critical CSS, defer the rest, so first paint doesn’t wait on a round trip
- Third-party tags via a container are invisible to the preload scanner. Budget for that — Tag Manager Performance
- Validate the markup when layout misbehaves inexplicably. Foster parenting and auto-closing account for a fair share of “the CSS isn’t working”