Lesson 04 / 25
Resource Loading Order
How script and style files hold up parsing and paint; comparing defer and async loading modes through an event timeline.
Contents
The previous lesson assumed the tree is built from the token stream without interruption. Real documents break this assumption: while parsing is under way, the document refers to resources beyond itself, and some of these references halt parsing.
The What the Browser Does lesson in the How the Internet Works course showed that these subresource requests exist: when an address was typed, the server saw more than one request. This lesson takes up when those requests are made and how they affect the document being processed. This is not a performance detail, it is a constraint that decides how the document gets written.
A Reference Is Born During Parsing
The browser reads the document from start to finish. The moment it sees a link element it
sends the style file’s request, the moment it sees an img element it sends the image’s
request. It does not wait for the end of the document. This has a direct consequence: a
resource’s place in the document decides the moment it is requested. A style file
written at the end of the document is requested later than one written at the start.
Resources do not have the same effect on parsing. There are three behaviors.
| Resource | Halts parsing | Holds up paint |
|---|---|---|
| Image | No | No |
Style file (link rel="stylesheet") |
No | Yes |
| Script (no attribute written) | Yes | Indirectly |
Script (defer) |
No | No |
Script (async) |
While executing | Indirectly |
Why a Script Halts Parsing
When a script element with no attribute written is seen, parsing halts, the script is downloaded, executed, and only then does parsing continue. This behavior is not a shortcoming, it is a necessity: the script can read the tree built up to that moment and add nodes to it. If the parser ran at the same time, the tree the script sees and the tree the parser is building would collide.
The price of this necessity is that the rest of the document is not processed while the script downloads. There are two ways to avoid paying that price, and both give a promise about when the script will reach the tree.
Defer (defer): the script downloads without halting parsing, and executes once parsing
has finished. Multiple deferred scripts execute in the order they are written in the
document. This is the only safe mode for scripts that depend on one another.
Async (async): the script downloads without halting parsing, and executes the moment it
finishes downloading; parsing halts while it executes. Execution order depends on download
order, not the document’s order. It is for scripts that run on their own, with no dependency
on other scripts.
This distinction is not a preference, it is a contract: writing async means “my order does
not matter.” If this attribute is written on a script whose order does matter, the bug
surfaces only under certain conditions and is hard to reproduce.
Why a Style File Holds Up Paint
A style file does not halt parsing — tree building continues. But paint cannot happen until style computation is complete; if it did, the page would first appear unstyled, then jump to its styled appearance. The browser waits instead.
There is a second, less well-known rule too: a script does not execute while a style file is pending. The reasoning is the same kind. A script can ask for an element’s computed style value; if there is a rule that has not been applied yet, the answer it gets would be wrong. For this reason a script is held back until the style file arrives, even if the script itself has already finished downloading.
Event Timeline
The script below runs these rules as an event model. It is not a real browser, it is a model of the rules: every piece of the document holds a unit of parsing time, every resource’s fetch time is given.
// loading.mjs — event model of the parsing/loading rules const document_ = [ { kind: "markup", name: "head start", duration: 1 }, { kind: "resource", name: "style.css", format: "style", fetch: 6 }, { kind: "markup", name: "body first half", duration: 2 }, { kind: "resource", name: "measurement.js", format: "script", mode: "blocking", fetch: 3, exec: 1 }, { kind: "markup", name: "body second half", duration: 2 }, { kind: "resource", name: "chart.js", format: "script", mode: "deferred", fetch: 5, exec: 1 }, { kind: "markup", name: "body end", duration: 1 }, ]; const log = []; let clock = 0; const deferred = []; let styleReady = Infinity; for (const part of document_) { if (part.kind === "markup") { log.push([clock, clock + part.duration, "parsing: " + part.name]); clock += part.duration; continue; } if (part.format === "style") { log.push([clock, clock, "request: " + part.name + " (in background)"]); styleReady = clock + part.fetch; continue; } if (part.mode === "blocking") { const fetchEnd = clock + part.fetch; const execStart = Math.max(fetchEnd, styleReady); log.push([clock, fetchEnd, "HALTED: fetching " + part.name]); if (execStart > fetchEnd) { log.push([fetchEnd, execStart, "HALTED: pending style"]); } log.push([execStart, execStart + part.exec, "exec: " + part.name]); clock = execStart + part.exec; continue; } if (part.mode === "deferred") { log.push([clock, clock, "request: " + part.name + " (in background)"]); deferred.push({ ...part, ready: clock + part.fetch }); } } const parsingEnd = clock; log.push([clock, clock, "parsing complete"]); for (const script of deferred) { const start = Math.max(clock, script.ready); log.push([start, start + script.exec, "exec (deferred): " + script.name]); clock = start + script.exec; } for (const [start, end, name] of log) { console.log(String(start).padStart(3) + " - " + String(end).padStart(3) + " " + name); } console.log("parsing end:", parsingEnd, "| all scripts done:", clock);
0 - 1 parsing: head start 1 - 1 request: style.css (in background) 1 - 3 parsing: body first half 3 - 6 HALTED: fetching measurement.js 6 - 7 HALTED: pending style 7 - 8 exec: measurement.js 8 - 10 parsing: body second half 10 - 10 request: chart.js (in background) 10 - 11 parsing: body end 11 - 11 parsing complete 15 - 16 exec (deferred): chart.js parsing end: 11 | all scripts done: 16
The timeline makes three things visible. First, measurement.js halted parsing for three
units (the 3–6 span) and, even after it finished downloading, waited one more unit; what it
was waiting for was style.css being ready. Second, because chart.js was deferred, it
never halted parsing at all, its request went out at unit 10, and it executed only after
parsing finished. Third, when parsing finishes (11) and when the scripts finish (16) are
separate moments; when the document becomes readable, the whole behavior layer may not yet be
ready.
The numbers are this model’s assumptions; real durations depend on network conditions and file sizes. What does not change is the ordering.
Placement Decisions
The rules lead to a few direct consequences.
A style file is written in the document’s head section. If it is requested late, paint starts late; if it is written in the middle of the body, content that has been painted up to that point gets restyled afterward.
A script mode that does not halt parsing is preferred over a script with no attribute written. Writing the script at the end of the document also saves most of parsing, but it does not start the download as early as defer does: a script written at the end only has its request go out once the parser reaches that point.
The browser does not sit idle while the parser is halted. A mechanism that scans ahead through the rest of the document, only to look for resource references, sends the requests it finds early. This behavior relies on resources being explicitly declared in the document: a resource whose address a script computes and produces is not visible to this scan.
Summary
- Resource requests are born during parsing; a resource’s place in the document decides the moment it is requested.
- A script with no attribute written halts parsing, because the script can read and change the tree built up to that moment.
- Defer executes the script after parsing finishes and in the document’s order; async mode executes it the moment it finishes downloading and gives no ordering guarantee.
- A style file does not halt parsing but holds up paint; a script also does not execute while a style file is pending.
- When parsing finishes and when the scripts finish are separate moments; the document being readable does not mean the whole behavior layer is ready.
Next Step
This topic established how the document is processed: bytes turning into a tree, the tree turning into boxes, resources’ effect on this chain. The next topic begins writing the document itself. The first question is every document’s outermost structure: what the document type declaration is for, what separates the head and body sections, and what the parser builds when none of these is written at all.
To keep your progress and take notes, Log in
My notes
Log in to take notes.