Tags: web-dev concept

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 parsingExecution orderRuns when
plainYesDocument orderImmediately on fetch
asyncNoWhichever downloads firstAs soon as it arrives
deferNoDocument order, guaranteedAfter parsing, before DOMContentLoaded
type="module"NoDocument orderLike 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

  • defer on everything of your own; async only 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”