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?
answer
- the parser stops at the tag
- download and execution both stall it
- nothing below the tag exists yet
- document.write is the reason
- bottom of body, or defer
basics
~20 sA 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 sA `<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<!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
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.
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.
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.
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