Why is a third-party snippet that calls document.write() from a <script> tag a problem for HTML page loading, and what happens if the same call runs after the document has finished parsing?
answer
- writes into the parser's input stream
- the preload scanner cannot see it
- one timing makes the call vanish
- another timing clears the whole page
basics
~20 sdocument.write() injects markup at the parser's current position, so the parser must stop and tokenise it — and any script it injects blocks the parser again. After parsing has finished, the same call implicitly reopens the document and wipes the existing page.
solid answer
~50 sDuring parsing, `document.write()` inserts a string at the parser's insertion point, so the parser has to stop, tokenise the new markup, and only then continue. If that markup contains a `<script src>` — the common third-party ad or tag-manager pattern — the browser has to discover, download and execute an extra file mid-parse, a request no preload scanner could have started earlier. That serialised round trip is why Chrome ships an intervention that can simply refuse to run parser-blocking scripts injected this way for users on slow connections, so the snippet silently does nothing. There are two more failure modes. From an `async` or deferred script, `document.write()` is ignored outright, because the insertion point is undefined at that time. And once the document has finished loading, a `document.write()` call implicitly performs `document.open()`, which clears the existing document and replaces it with what you write — the classic "my page went blank" bug. The fix is to insert nodes with DOM APIs instead.
code
html · 11 lines<p>before</p>
<script>
// written into the parser's input stream at this exact point
document.write('<p>injected during parsing</p>');
</script>
<p>after</p>
<!-- from here the write is ignored: no insertion point -->
<script async>
document.write('<p>never appears</p>');
</script>go deeper
Know that document.write() writes into the page while it is being parsed, that it is considered legacy, and that calling it after the page has loaded erases the page.
Explain the mechanism: the string enters the parser's input stream at the insertion point, must be tokenised before parsing resumes, and any script tag inside it becomes another blocking fetch.
Show the production angle: injected scripts are invisible to the preload scanner, a browser intervention can refuse to run them on slow connections so vendor data silently goes missing, and writes from async or deferred scripts are dropped entirely.
Own the third-party policy: which vendor tags may ship, requiring asynchronous loaders or iframe containment, and how you measure the tail-latency and data-loss risk of a tag chain before it reaches the page template.
## What document.write actually does `document.write()` does not append to the page. It writes a string **into the parser's input stream** at the current insertion point — the position the HTML parser has reached. The parser then tokenises that string as if it had arrived over the network at exactly that spot. This is why the API exists at all: it predates the DOM and was designed for a document being streamed and parsed top to bottom. ```html <p>before</p> <script>document.write('<p>injected</p>');</script> <p>after</p> ``` The injected paragraph lands between the other two, because that is where the parser was. ## Why it hurts loading The script itself is already parser-blocking. On top of that, the written markup must be tokenised before parsing can continue, and if it contains a script tag the browser must fetch that file too — synchronously, from a URL it could not know about in advance. Browsers normally soften blocking scripts with a preload scanner: while the parser is stalled, a lookahead scanner reads further down the raw bytes and starts fetching the images, stylesheets and scripts it finds. Markup that does not exist until a script runs is invisible to that scanner. So a `document.write`-injected script is a fully serialised round trip: parse, stall, fetch, run, write, fetch again. Chains of these — a tag manager writing a vendor tag which writes another — are a well-known source of multi-second stalls. That cost is severe enough that Chrome shipped an intervention: for users on slow connections, it may block a parser-blocking script that was injected via `document.write` from a cross-origin URL, logging a console warning instead of running it. The practical consequence is that a snippet written this way is not merely slow — it may not execute at all for part of your audience, and the vendor's dashboard shows missing data with no error on your side. ## The ignored case If the calling script is `async`, deferred, or a module, its `document.write()` calls are ignored. At that moment the parser has no insertion point for the script, so the write is discarded. This produces one of the more confusing symptoms in the wild: adding `async` to a vendor tag to "make it faster" makes it stop working entirely, with no exception thrown. ## The destructive case After the document has finished loading, calling `document.write()` implicitly calls `document.open()` first. `document.open()` clears the document — the entire existing DOM, and the event listeners attached to it — and starts a fresh one to receive what you write. So a call from a click handler, a `setTimeout`, or a late-arriving callback blanks the page and replaces it with the written string. Nothing crashes; the page just disappears. ```javascript button.addEventListener('click', () => { document.write('hello'); // wipes the page it was clicked on }); ``` ## What to do instead Insert content by building nodes and attaching them, or by setting markup on an element you own. For loading another script, create the element and append it rather than writing a tag string — that path is asynchronous by default and never touches the parser's input stream. When a vendor's snippet insists on `document.write`, the options are: ask for their asynchronous loader (most now have one), load their tag inside an iframe so the destructive write is contained, or drop the tag. ## Answering well A strong answer separates the three states — during parsing it is parser-blocking and hides work from the preload scanner; from async or deferred code it is ignored; after load it destroys the document — and finishes with the concrete modern consequence, that a browser may refuse to run the injected script at all on a slow connection.
- A vendor tag works when loaded normally but stops working the moment you add async to it. What is the likely cause?The snippet almost certainly calls `document.write()` to inject its real script tag. Once the script is `async`, it executes outside parsing, the insertion point is undefined, and the write is silently discarded — no error, no output. The fix is the vendor's asynchronous loader, which appends a script element instead of writing markup.
- Why can't the preload scanner help with a script injected by document.write?The scanner works by reading raw bytes ahead of the parser and starting fetches for URLs it finds there. A URL that only exists after a script executes is not in those bytes, so nothing can be requested early. The fetch is therefore fully serialised behind the blocking script that wrote it.
- What exactly is destroyed when document.write runs after the page has loaded?The implicit `document.open()` clears the current document — its nodes and the listeners bound to them — and starts a new one to receive the written string. Anything holding references to old nodes is left pointing at a detached tree, and the visible page is replaced by whatever you wrote.
saying these in an interview costs you the question
- Says document.write appends to the end of body
- Thinks adding async makes the vendor snippet safe
- Believes the preload scanner still fetches injected scripts
- Says a post-load write only appends extra text
- Treats it as merely slow, never as blocked