Friday, February 3, 2023
HomeCSSWhy Not doc.write()? – CSS Wizardry – Net Efficiency Optimisation

Why Not doc.write()? – CSS Wizardry – Net Efficiency Optimisation


Written by on CSS Wizardry.

Desk of Contents
  1. What Makes Scripts Gradual?
  2. The Preload Scanner
  3. doc.write() Hides Information From the Preload Scanner
    1. What About Async Snippets?
  4. doc.write() Executes Synchronously
  5. Is It All Unhealthy?
    1. Early doc.write()
    2. Late doc.write()
  6. It Will get Worse…
  7. Keep away from doc.write()

In the event you’ve ever run a Lighthouse check earlier than, there’s a excessive probability you’ve seen
the audit Keep away from
doc.write()
:

For customers on gradual connections, exterior scripts dynamically
injected by way of `doc.write()` can delay web page load by tens of
seconds.

You’ll have additionally seen that there’s little or no clarification as to why
doc.write() is so dangerous. Effectively, the brief reply is:

From a purely performance-facing viewpoint, doc.write() itself
isn’t that particular or distinctive.
In truth, all it does is surfaces potential
behaviours already current in any synchronous script—the one essential distinction is
that doc.write() ensures that these unfavorable behaviours will manifest
themselves, whereas different synchronous scripts could make use of alternate
optimisations to sidestep them.

N.B. This audit and, accordingly, this text, solely offers with
script injection utilizing doc.write()—not its utilization on the whole. The MDN
entry for
doc.write()

does an excellent job of discouraging its use.

What Makes Scripts Gradual?

There are a variety of issues that may make common, synchronous scripts
gradual:

  1. Synchronous JS can block DOM building whereas the file is downloading.
    • The assumption that synchronous JS blocks DOM building is just true
      in sure situations.
  2. Synchronous JS all the time blocks DOM building whereas the file is
    executing.

    • It runs in-situ on the actual level it’s outlined, so something outlined after
      the script has to attend.
  3. Synchronous JS by no means blocks downloads of subsequent information.
    • This has been true for nearly 15 years
      on the time of writing, but nonetheless stays a typical false impression amongst
      builders. That is carefully associated to the primary level.

The worst case state of affairs is a script that falls into each (1) and (2), which is
extra more likely to have an effect on scripts outlined earlier in your HTML. doc.write(),
nevertheless, forces scripts into each (1) and (2) no matter once they’re
outlined.

The Preload Scanner

The rationale scripts by no means block subsequent downloads is due to one thing
known as the Preload Scanner. The Preload Scanner is a secondary, inert,
download-only parser that’s liable for working down the HTML and
asynchronously requesting any out there subresources it would discover, mainly
something contained in src or href attributes, together with pictures, scripts,
stylesheets, and so on. Because of this, information fetched by way of the Preload Scanner are
parallelised, and could be downloaded asynchronously alongside different (doubtlessly
synchronous) sources.

The Preload Scanner is decoupled from the first parser, which is accountable
for establishing the DOM, the CSSOM, working scripts, and so on. Which means
a big majority of information we fetch are carried out so asynchronously and in
a non-blocking method, together with some synchronous scripts. This is the reason not all
blocking scripts block throughout their obtain part—they might have been fetched by
the Preload Scanner earlier than they have been really wanted, thus in a non-blocking
method.

The Preload Scanner and the first parser start processing the HTML at
more-or-less the identical time, so the Preload Scanner doesn’t actually get a lot of
a head begin. This is the reason early scripts usually tend to block DOM
building throughout their obtain part than late scripts: the first parser
is extra more likely to encounter the related <script src> factor whereas the file
is downloading if the <script src> factor is early within the HTML. Late (e.g.
at-</physique>) synchronous <script src>s usually tend to be fetched by the
Preload Scanner whereas the first parser remains to be hung up doing work earlier in
the web page.

Put merely, scripts outlined earlier within the web page usually tend to block on
their obtain than later ones; later scripts usually tend to have been
fetched preemptively and asynchronously by the Preload Scanner.

doc.write() Hides Information From the Preload Scanner

As a result of the Preload Scanner offers with tokeniseable src and href attributes,
something buried in JavaScript is invisible to it:

<script>
  doc.write('<script src=file.js></script>')
</script>

This isn’t a reference to a script; this can be a string in JS. Which means the
browser can’t request this file till it’s really run the <script> block
that inserts it, which could be very a lot just-in-time (and too late).

doc.write() forces scripts to dam DOM building throughout their
obtain by hiding them from the Preload Scanner.

What About Async Snippets?

Async snippets such because the one under endure the identical destiny:

<script>
  var script = doc.createElement('script');
  script.src = 'file.js';
  doc.head.appendChild(script);
</script>

Once more, file.js isn’t a filepath—it’s a string! It’s not till the browser has
run this script that it places a src attribute into the DOM and might then request
it. The first distinction right here, although, is that scripts injected this fashion are
asynchronous by default. Regardless of being hidden from the Preload Scanner, the
affect is negligible as a result of the file is implicitly asynchronous anyway.

That stated, async snippets are nonetheless an
anti-pattern
—don’t use them.

doc.write() Executes Synchronously

doc.write() is sort of completely used to conditionally load
a synchronous script. In the event you simply want a blocking script, you’d use a easy
<script src> factor:

<script src=file.js></script>

In the event you wanted to conditionally load an asynchronous script, you’d add
some if/else logic to your async snippet.

<script>

  if (situation) {
    var script = doc.createElement('script');
    script.src = 'file.js';
    doc.head.appendChild(script);
  }

</script>

If it is advisable to conditionally load a synchronous script, you’re kinda caught…

Scripts injected with, for instance, appendChild are, per the spec,
asynchronous. If it is advisable to inject a synchronous file, one of many solely
easy choices is doc.write():

<script>

  if (situation) {
    doc.write('<script src=file.js></script>')
  }

</script>

This ensures a synchronous execution, which is what we would like, nevertheless it additionally
ensures a synchronous fetch, as a result of that is hidden from the Preload Scanner,
which is what we don’t need.

doc.write() forces scripts to dam DOM building throughout their
execution by being synchronous by default.

Is It All Unhealthy?

The placement of the doc.write() in query makes an enormous distinction.

As a result of the Preload Scanner works most successfully when it’s coping with
subresources later within the web page, doc.write() earlier within the HTML is much less
dangerous.

Early doc.write()

<head>

  ...

  <script>
    doc.write('<script src=https://slowfil.es/file?kind=js&delay=1000></script>')
  </script>

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

  ...

</head>

In the event you put a doc.write() because the very very first thing in your <head>, it’s
going to behave the very same as an everyday <script src>—the Preload Scanner
wouldn’t have had a lot of a head begin anyway, so we’ve already missed out on
the prospect of an asynchronous fetch:

doc.write() as the very first thing within the <head>. FCP is at 2.778s.

Above, we see that the browser has managed to parallelise the requests: the
major parser ran and injected the doc.write(), whereas the Preload
Scanner fetched the CSS.

Owing to CSS’ Highest precedence, it’s going to all the time be requested earlier than
Excessive precedence JS, no matter the place every is outlined.

If we exchange the doc.write() with a easy <script src>, we’d see the
very same behaviour, that means on this particular occasion, doc.write() is
no extra dangerous than an everyday, synchronous script:

<head>

  ...

  <script src=https://slowfil.es/file?kind=js&delay=1000></script>

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

  ...

</head>

This yields an equivalent waterfall:

Utilizing a syncrhonous <script src> as an alternative of doc.write(). FCP is at 2.797s.

As a result of the Preload Scanner was unlikely to seek out both variant, we don’t
discover any actual degradation.

Late doc.write()

<head>

  ...

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

  <script>
    doc.write('<script src=https://slowfil.es/file?kind=js&delay=1000></script>')
  </script>

  ...

</head>

As a result of JS can write/learn to/from the CSSOM, all browsers will halt execution of
any synchronous JS if there’s any previous, pending CSS. In impact, CSS
blocks
JS
,
and on this instance, serves to cover the doc.write() from the Preload
Scanner.

Thus, doc.write() later within the web page does turn out to be extra extreme. Hiding
a file from the Preload Scanner—and solely surfacing it to the browser the precise
second we want it—goes to make its whole fetch a blocking motion. And,
as a result of the doc.write() file is now being fetched by the first parser
(i.e. the principle thread), the browser can’t full some other work whereas the file
is on its approach. Blocking on high of blocking.

As quickly as we cover the script file from the Preload Scanner, we discover
drastically totally different behaviour. By merely swapping the doc.write() and
the rel=stylesheet round, we get a a lot, a lot slower expertise:

doc.write() late within the <head>. FCP is at 4.073s.

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

It Will get Worse…

The entire purpose I’m scripting this submit is that I’ve a consumer in the meanwhile who
is utilizing doc.write() late within the <head>. As we now know, this pushes
each the fetch and the execution on the principle thread. As a result of browsers are
single-threaded, because of this not solely are we incurring community delays
(due to a synchronous fetch), we’re additionally leaving the browser unable to work
on anything for the complete period of the script’s obtain!

The primary thread goes fully silent throughout the injected file’s
fetch. This doesn’t occur when information are fetched from the Preload Scanner.

Keep away from doc.write()

In addition to exhibiting unpredictable and buggy behaviour as keenly harassed in
the MDN and
Google articles,
doc.write() is gradual. It ensures each a blocking fetch and a blocking
execution, which holds up the parser for much longer than obligatory. Whereas it
doesn’t introduce any new or distinctive efficiency points per se, it simply forces
the worst of all worlds.

Keep away from doc.write() (however not less than now why).




☕️ Did this assist? Purchase me a espresso!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments