skip to content

Script Loading and Parser Blocking

You will learn exactly what a classic script does to the parser and how async, defer, and module change that. Interviewers ask the async-vs-defer question constantly and expect execution-order detail, not slogans.

on this pageshow

questions

5

An HTML document has <script src="app.js"></script> in its <head> with no other attributes. What does the HTML parser do when it reaches that tag, and why does moving the same tag to just before </body> change what the user sees?

level: juniorimportance: must knowfreq 80%

answer

  1. the parser stops at the tag
  2. download and execution both stall it
  3. nothing below the tag exists yet
  4. document.write is the reason
  5. bottom of body, or defer

basics

~20 s

A classic script with no attributes is parser-blocking: the parser stops at the tag, waits for app.js to download and execute, then resumes. Placed before </body> the whole document is already parsed, so the user sees content first.

solid answer

~50 s

A `<script src>` with no `async`, `defer`, or `type="module"` is a classic parser-blocking script. When the HTML parser reaches it, it stops building the DOM, fetches `app.js` over the network, hands it to the JavaScript engine, waits for it to finish executing, and only then continues with the rest of the markup. Nothing after that tag exists as DOM yet, so nothing after it can be shown, and a head script also sees an empty body — `document.querySelector('.hero')` returns `null`. The reason the browser must behave this way is `document.write()`: a classic script can inject markup at its own position in the token stream, so the parser cannot safely run ahead of it. Moving the tag to just before `</body>` means the entire document is parsed and displayable before the script runs, which is the pre-`defer` way of avoiding the stall.

code

html · 11 lines
html
<!doctype html>
<title>Blocking vs deferred</title>
<head>
  <!-- parser stops here for the whole fetch and execution -->
  <script src="blocking.js"></script>
  <!-- discovered just as early, but executed after parsing -->
  <script src="deferred.js" defer></script>
</head>
<body>
  <h1>Content below the blocking script waits for it</h1>
</body>

go deeper

for a junior

Be able to say the word "parser-blocking" and explain that the browser stops building the page until the script has downloaded and run. Know the classic fix of placing the tag before the closing body tag.

for a middle

Explain both halves of the cost — network fetch and main-thread execution — and give document.write as the reason synchronous execution is the default. Note that async and defer are ignored on inline scripts.

for a senior

Be ready to diagnose a slow first paint back to a specific blocking tag and argue for defer or module loading instead of end-of-body placement, including what late discovery costs on a large document.

for a principal

Own the policy: which scripts are allowed to block a page at all, how third-party tags get held to it, and how you keep a blocking tag from creeping back into the head as a template is edited by many teams.

## What "parser-blocking" actually means The browser parses an HTML response as a stream, building DOM nodes as bytes arrive. A `<script>` element with no `async`, no `defer`, and no `type="module"` — a *classic, parser-inserted* script — interrupts that stream. The parser stops adding nodes at exactly that point, hands control to the JavaScript engine, and does not resume until the script has finished running. When the element has a `src`, the pause covers the network round trip as well as the execution. ```html <head> <script src="app.js"></script> </head> <body> <h1>Everything here is still unparsed bytes</h1> </body> ``` While `app.js` is in flight, the `<h1>` is not a node, not styled, and not paintable. It is still text sitting in a buffer. ## Why the browser is forced to do this This is a correctness requirement, not an oversight. A classic script may call `document.write()`, which inserts markup into the token stream *at the script's own position*. Because the script can change the very bytes the parser is about to consume, the parser is not allowed to run ahead of it. That one legacy API is why synchronous execution is the default, and why `async` and `defer` had to be introduced as explicit opt-ins rather than simply becoming the new behaviour. ## What the user experiences DOM construction is a prerequisite for showing anything, so a blocked parser is a blocked page. A script in `<head>` that needs 800 ms to fetch on a slow mobile connection leaves the user looking at a blank or stale screen for 800 ms, even though the bytes for the header, nav, and hero arrived long ago. Two separate costs are hiding in that number: the *fetch* (waiting on the network) and the *execution* (parsing and running the JavaScript, which occupies the main thread). Both happen with the parser stopped, and both scale with the script's size. Modern browsers soften the first cost: while blocked, they scan ahead in the raw response bytes for resource URLs and start those downloads early. That is a download optimisation only — the script still executes in place, in source order, and the parser still waits for it. ## The second consequence: a head script sees an empty document Because the parser has not reached the body yet, anything the script queries in the document is missing. `document.getElementById('root')` is `null`, `document.body` may itself be `null`, and adding a listener to a button that has not been parsed silently does nothing. Code that runs in `<head>` therefore has to defer its DOM work until parsing has finished, which is the origin of the habit of wrapping everything in a "wait for the document" callback. ## The end-of-body placement Putting the tag immediately before `</body>` sidesteps both problems without any attribute: by the time the parser reaches it, the whole document has been built, so the content is already displayable and every element the script wants is in the DOM. The tradeoff is *discovery time* — the browser cannot begin the download until it has parsed all the markup above it, so on a large page the request starts late. The scan-ahead behaviour recovers much of that, but not all of it, and the script still runs synchronously once it arrives. ## What you would actually write today `defer` on the same tag in `<head>` gives you both halves: the element is discovered immediately so the download starts at once, the fetch happens in parallel with parsing, and execution is postponed until parsing is complete, in document order. `type="module"` behaves as deferred by default. The end-of-body trick is still correct and still common in hand-written HTML, but it is a workaround from before those attributes existed. One detail that trips people up: `async` and `defer` are ignored on an **inline** classic script. `<script defer>init()</script>` with no `src` runs immediately and synchronously, blocking the parser exactly like an unattributed one. There is no download to parallelise, but the execution stall is real, and a long inline script in `<head>` is a genuine cause of slow first paints. ## How to answer this in an interview Say "parser-blocking", name both costs (download *and* execution), give `document.write` as the reason the default exists, and finish with the modern fix rather than only the historical one. That is the complete answer; "scripts go at the bottom" on its own is the slogan version.

  • Does an inline `<script>` with no src block the parser as well?
    Yes. There is no download to wait for, but the JavaScript still runs synchronously at that point in the stream and the parser waits for it to return. `async` and `defer` are ignored on inline classic scripts, so a large inline script in `<head>` blocks exactly like an external one.
  • Why does a script in the head see none of the page's body elements?
    Because the parser stops at the script tag, everything below it — including the entire body — has not been turned into nodes yet. Queries return `null`, and `document.body` itself may be `null`. The script has to wait until parsing completes before touching elements.
  • If the browser can start downloading a script before the parser reaches it, why does placement still matter?
    Scan-ahead only warms the network. Execution order is fixed by document position, and a classic script still runs in place with the parser stopped. So an early script may be fully downloaded and still stall the parse while it executes.

saying these in an interview costs you the question

  • Says only the download blocks, not the execution
  • Claims scripts always download in parallel with parsing
  • Thinks defer on an inline script postpones it
  • Believes end-of-body placement makes a script asynchronous
  • Assumes a head script can query body elements

context

open as a page

For an external classic script in HTML — <script src="a.js"></script> — what is the difference between adding the async attribute and adding the defer attribute, both in when the script executes and in what ordering guarantees you get across several such scripts?

level: middleimportance: must knowfreq 88%

basics

~10 s

Both download in parallel with parsing. An async script executes the moment its download finishes, interrupting the parser, in unpredictable order. A defer script executes only after parsing completes, in document order.

open as a page

In HTML, how does <script type="module" src="app.js"></script> behave with respect to the parser compared with a classic <script src="app.js"></script>, and what effect do the defer and async attributes have on a module script?

level: middleimportance: should knowfreq 50%

basics

~20 s

A module script is deferred by default: it never blocks the parser and runs after parsing, in document order. The defer attribute is therefore redundant, while async still works and makes it run as soon as its graph is ready — including on inline module scripts.

open as a page

A script element is created with document.createElement('script'), given a src, and appended to the head at runtime. Does it block the HTML parser, and in what order do several scripts created this way execute?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A dynamically inserted script never blocks the HTML parser and behaves as async by default: each one runs as soon as its own download finishes, so order is unpredictable. Setting script.async = false before appending restores insertion-order execution.

open as a page

Why is document.write() treated as unsafe on a modern web page — what happens when it runs after the document has finished parsing, and what does it do to loading when it is used to inject a <script src> tag?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Called during parsing it injects markup into the token stream, but called after the document has loaded it implicitly reopens the document and wipes everything already there. Injecting a script tag with it also forces a parser-blocking fetch on the critical path.

open as a page