---
layout: post
title: "Why Not document.write()?"
date: 2023-01-10 16:17:11
categories: Web Development
main: "https://csswizardry.com/wp-content/uploads/2023/01/lighthouse.png"
meta: "We’re often told not to use document.write(), but… why?!"
faq:
- question: "Why is document.write() bad for performance?"
answer: "document.write() forces scripts to block DOM construction during their download by hiding them from the Preload Scanner. This means that scripts injected with document.write() are always fully synchronous."
last_modified_at: 2025-06-01
---
If you’ve ever run a Lighthouse test before, there’s a high chance you’ve seen
the audit [Avoid
`document.write()`
](https://developer.chrome.com/docs/lighthouse/best-practices/no-document-write/):
For users on slow connections, external scripts dynamically
injected via
document.write() can delay page load by tens of
seconds.document.write() specifically will impact Interaction to Next
Paint. Only if the JavaScript itself is particularly problematic or expensive
will you suffer poor INP scores. If you are struggling with INP, let’s organise
a workshop!
**N.B.** This audit and, accordingly, this article, only deals with
script injection using `document.write()`—not its usage in general. [The MDN
entry for
`document.write()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/write)
does a good job of discouraging its use.
## What Makes Scripts Slow?
There are a number of things that can make regular, synchronous scripts[^1]
slow:
1. Synchronous JS **can** block DOM construction while the file is downloading.
* The belief that synchronous JS blocks DOM construction
is only true
in certain scenarios.
2. Synchronous JS **always** blocks DOM construction while the file is
executing.
* It runs in-situ at the exact point it’s defined, so anything defined after
the script has to wait.
3. Synchronous JS **never** blocks downloads of subsequent files.
* This has been true for [almost 15 years](https://www.stevesouders.com/blog/2008/03/10/ie8-speeds-things-up/)
at the time of writing, yet still remains a common misconception among
developers. This is closely related to the first point.
The worst case scenario is a script that falls into both (1) and (2), which is
more likely to affect scripts defined earlier in your HTML. `document.write()`,
however, forces scripts into both (1) and (2) regardless of when they’re
defined.
## The Preload Scanner
The reason scripts never block subsequent downloads is because of something
called the _Preload Scanner_. The Preload Scanner is a secondary, inert,
download-only parser that’s responsible for running down the HTML and
asynchronously requesting any available subresources it might find, chiefly
anything contained in `src` or `href` attributes, including images, scripts,
stylesheets, etc. As a result, files fetched via the Preload Scanner are
parallelised, and can be downloaded asynchronously alongside other (potentially
synchronous) resources.
The Preload Scanner is decoupled from the primary parser, which is responsible
for constructing the DOM, the CSSOM, running scripts, etc. This means that
a large majority of files we fetch are done so asynchronously and in
a non-blocking manner, including some synchronous scripts. This is why not all
blocking scripts block during their download phase—they may have been fetched by
the Preload Scanner before they were actually needed, thus in a non-blocking
manner.
The Preload Scanner and the primary parser begin processing the HTML at
more-or-less the same time, so the Preload Scanner doesn’t really get much of
a head start. This is why early scripts are more likely to block DOM
construction during their download phase than late scripts: the primary parser
is more likely to encounter the relevant `
```
This is not a reference to a script; this is a string in JS. This means that the
browser can’t request this file until it’s actually run the `
```
Again, `file.js` is not a filepath—it’s a string! It’s not until the browser has
run this script that it puts a `src` attribute into the DOM and can then request
it. The primary difference here, though, is that scripts injected this way are
asynchronous by default. Despite being hidden from the Preload Scanner, the
impact is negligible because the file is implicitly asynchronous anyway.
That said, [async snippets are still an
anti-pattern](/2022/10/speeding-up-async-snippets/)—don’t use them.
## `document.write()` Executes Synchronously
`document.write()` is almost exclusively used to conditionally load
a synchronous script. If you just need **a blocking script**, you’d use a simple
`
```
If you needed to **conditionally load an asynchronous script**, you’d add
some `if`/`else` logic to your async snippet.
```html
```
If you need to **conditionally load a synchronous script**, you’re kinda stuck…
Scripts injected with, for example, `appendChild` are, per the spec,
asynchronous. If you need to inject a synchronous file, one of the only
straightforward options is `document.write()`:
```html
```
This guarantees a synchronous execution, which is what we want, but it also
guarantees a synchronous fetch, because this is hidden from the Preload Scanner,
which is what we don’t want.
**`document.write()` forces scripts to block DOM construction during their
execution by being synchronous by default.**
## Is It All Bad?
The location of the `document.write()` in question makes a huge difference.
Because the Preload Scanner works most effectively when it’s dealing with
subresources later in the page, `document.write()` earlier in the HTML is less
harmful.
### Early `document.write()`
```html
<script src> instead of document.write(). FCP is at 2.797s.
document.write() late in the <head>. FCP is at 4.073s.