Home / Uncategorized / Why Not document.write()? – Web Performance and Site Speed Consultant

Why Not document.write()? – Web Performance and Site Speed Consultant


Why Not document.write()? – Web Performance and Site Speed Consultant








Arrange a Masterclass

Written by on CSS Wizardry.

Table of Contents
  1. What Makes Scripts Slow?
  2. The Preload Scanner
  3. document.write() Hides Files From the Preload Scanner
    1. What About Async Snippets?
  4. document.write() Executes Synchronously
  5. Is It All Bad?
    1. Early document.write()
    2. Late document.write()
  6. It Gets Worse…
  7. Avoid document.write()

If you’ve ever run a Lighthouse test before, there’s a high chance you’ve seen
the audit Avoid
document.write()
:

For users on slow connections, external scripts dynamically
injected via document.write() can delay page load by tens of
seconds.

You may have also seen that there’s very little explanation as to why
document.write() is so harmful. Well, the short answer is:

From a purely performance-facing point of view, document.write() itself
isn’t that special or unique.
In fact, all it does is surfaces potential
behaviours already present in any synchronous script—the only main difference is
that document.write() guarantees that these negative behaviours will manifest
themselves, whereas other synchronous scripts can make use of alternate
optimisations to sidestep them.

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()

does a good job of discouraging its use.

What Makes Scripts Slow?

There are a number of things that can make regular, synchronous scripts
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
      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 /script>')

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 file.js>

If you needed to conditionally load an asynchronous script, you’d add some if/else logic to your async snippet.

/script>')
  }


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()



  ...

  /script>')
  

   rel=stylesheet href=https://slowfil.es/file?type=css&delay=1000>

  ...


If you put a document.write() as the very first thing in your , it’s going to behave the exact same as a regular https://slowfil.es/file?type=js&delay=1000> rel=stylesheet href=https://slowfil.es/file?type=css&delay=1000> ...

This yields an identical waterfall:

Using a syncrhonous /script>') ...

Because JS can write/read to/from the CSSOM, all browsers will halt execution of any synchronous JS if there is any preceding, pending CSS. In effect, CSS blocks JS, and in this example, serves to hide the document.write() from the Preload Scanner.

Thus, document.write() later in the page does become more severe. Hiding a file from the Preload Scanner—and only surfacing it to the browser the exact moment we need it—is going to make its entire fetch a blocking action. And, because the document.write() file is now being fetched by the primary parser (i.e. the main thread), the browser can’t complete any other work while the file is on its way. Blocking on top of blocking.

As soon as we hide the script file from the Preload Scanner, we notice drastically different behaviour. By simply swapping the document.write() and the rel=stylesheet around, we get a much, much slower experience:

document.write() late in the . FCP is at 4.073s.

Now that we’ve hidden the script from the Preload Scanner, we lose all parallelisation and incur a much larger penalty.

It Gets Worse…

The whole reason I’m writing this post is that I have a client at the moment who is using document.write() late in the . As we now know, this pushes both the fetch and the execution on the main thread. Because browsers are single-threaded, this means that not only are we incurring network delays (thanks to a synchronous fetch), we’re also leaving the browser unable to work on anything else for the entire duration of the script’s download!

The main thread goes completely silent during the injected file’s fetch. This doesn’t happen when files are fetched from the Preload Scanner.

Avoid document.write()

As well as exhibiting unpredictable and buggy behaviour as keenly stressed in the MDN and Google articles, document.write() is slow. It guarantees both a blocking fetch and a blocking execution, which holds up the parser for far longer than necessary. While it doesn’t introduce any new or unique performance issues per se, it just forces the worst of all worlds.

Avoid document.write() (but at least now you know why).






Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.


Suffering? Fix It Fast!


Connect


Projects

Next Appearance

I help teams achieve class-leading web performance, providing consultancy, guidance, and hands-on expertise.

I specialise in tackling complex, large-scale projects where speed, scalability, and reliability are critical to success.

CSS Wizardry Ltd is a company registered in England and Wales. Company No. 08698093, VAT No. 170659396. License Information.

Leave a Reply

Your email address will not be published. Required fields are marked *