<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>CSS Wizardry</title>
    <description>&amp;ndash; Harry Roberts &amp;ndash; Web Performance Consultant</description>
    <link>https://csswizardry.com/</link>
    <atom:link href="https://csswizardry.com/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Correctly Configure (Pre) Connections</title>
        <description>&lt;p&gt;A trivial performance optimisation to help speed up third-party or other-origin
requests is to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; them: hint that the browser should preemptively open
a full connection (&lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt;, &lt;b style=&quot;color: #eb8a30&quot;&gt;TCP&lt;/b&gt;, &lt;b style=&quot;color: #c94bd4&quot;&gt;TLS&lt;/b&gt;) to the origin in question,
for example:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://fonts.googleapis.com&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the right circumstances, this simple, single line of HTML can make pages
&lt;a href=&quot;https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/&quot;&gt;hundreds of milliseconds
faster&lt;/a&gt;!
But time and again, I see developers misconfiguring even this most basic of
features. Because, as is often the case, there’s much more to this ‘basic
feature’ than meets the eye. Let’s dive in…&lt;/p&gt;

&lt;h2 id=&quot;learn-by-example&quot;&gt;Learn by Example&lt;/h2&gt;

&lt;p&gt;At the time of writing, the &lt;a href=&quot;https://www.bbc.co.uk/news&quot;&gt;BBC News homepage&lt;/a&gt; (in
the UK, at least) has these four &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s defined early in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;//static.bbc.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;//m.files.bbci.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;//nav.files.bbci.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;//ichef.bbci.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;small&gt;Readers on narrow screens should know that each of these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s
also carries a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; attribute—scroll along to see for yourself!&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Note that the BBC use schemeless URLs (i.e. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href=//…&lt;/code&gt;). I would &lt;em&gt;not&lt;/em&gt;
recommend doing this. Always force HTTPS when it’s available.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Having consulted for the BBC a number of times, I know that they make heavy use
of internal subdomains to share resources across teams. While this suits
developer ergonomics, it’s not great for performance, particularly in cases
where the subdomain in question is on the critical path. Warming up connections
to important origins is a must for the BBC.&lt;/p&gt;

&lt;p&gt;However, a look at a waterfall tells me that none of these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s worked!&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;/wp-content/uploads/2023/12/bbc-news-waterfall-initial.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;318&quot; /&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Above, you can see that the browser discovered references to each of these
origins in the first chunk of HTML, before the 1-second mark. This is evidenced
by the light white bars that denote ‘waiting’ time—the browser knows it needs
the files, but is waiting to dispatch the requests. However, we can also see
that the browser didn’t begin network negotiation until closer to the 1.5-second
mark, when we begin seeing a tiny slither of green—&lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt;—followed by the much more costly &lt;b style=&quot;color: #eb8a30&quot;&gt;TCP&lt;/b&gt; and &lt;b style=&quot;color: #c94bd4&quot;&gt;TLS&lt;/b&gt;. What went wrong?!&lt;/p&gt;

&lt;h2 id=&quot;working-out-which-origins-to-preconnect&quot;&gt;Working Out Which Origins to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;In the example above, we have five connections to the following four domains
(more on that later):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nav.files.bbci.co.uk&lt;/code&gt;:&lt;/strong&gt; On the critical path with render-blocking CSS.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt;:&lt;/strong&gt; On the critical path with
render-blocking CSS and JS.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m.files.bbci.co.uk&lt;/code&gt;:&lt;/strong&gt; On the critical path with render-blocking CSS.
    &lt;ul&gt;
      &lt;li&gt;The screenshot above marks the CSS as non-blocking because of the way it’s
fetched—it’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preload&lt;/code&gt;ed, which &lt;em&gt;is&lt;/em&gt; non-blocking, but it’s then
conditionally applied to the page using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; (which is its own
&lt;a href=&quot;/2023/01/why-not-document-write/&quot;&gt;performance faux pas in itself&lt;/a&gt;).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ichef.bbci.co.uk&lt;/code&gt;:&lt;/strong&gt; Not on the critical path, but does host the
homepage’s LCP element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;small&gt;&lt;strong&gt;N.B.&lt;/strong&gt; For neatness, I am omitting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://&lt;/code&gt; from
written prose, but it is vital that you include the relevant scheme in your
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href&lt;/code&gt; attribute. All code examples are complete and correct.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Each of these four origins is vital to the page, so all four would be candidates
for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;. However, the BBC aren’t attempting to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt; at all; instead, they’re &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;ing
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.bbc.co.uk&lt;/code&gt;, which is also used, but &lt;em&gt;isn’t&lt;/em&gt; on the critical path. This
feels more like a simple oversight or a typo than anything else.&lt;/p&gt;

&lt;p&gt;As a rule, &lt;strong&gt;if the origin is important to the page and is used within the first
five seconds of the page-load lifecycle, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; it&lt;/strong&gt;. If the origin is not
important, don’t &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; it; if it is important but is used more than five
seconds into the page load lifecycle, your priority should be moving it sooner.&lt;/p&gt;

&lt;p&gt;Note that &lt;q&gt;important&lt;/q&gt; is very subjective. Your analytics isn’t important;
your chat client isn’t important. Your consent management platform is important;
your &lt;a href=&quot;https://cloudinary.com/&quot;&gt;image CDN is important&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One easy way to get an overview of early and important origins—and the method
I use when advising clients—is to use WebPageTest. Once you’ve run a test, you
can head to a &lt;a href=&quot;https://www.webpagetest.org/result/231209_AiDc18_7GP/2/details/#connectionView_fv_1&quot;&gt;&lt;em&gt;Connection
View&lt;/em&gt;&lt;/a&gt;
of the waterfall which shows a diagram comprising entries per origin, not per
response:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/12/bbc-news-waterfall-connections.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;335&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Note that some connections are actually shared across more than one
domain: this is &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.1&quot;&gt;HTTP2’s
connection coalescence&lt;/a&gt;, available when origins share the same IP address and
certificates.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;As easy as that—that’s your list of potential origins!&lt;/p&gt;

&lt;h2 id=&quot;dont-preconnect-too-many-origins&quot;&gt;Don’t &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; Too Many Origins&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; should be used sparingly. Connection overhead isn’t &lt;em&gt;huge&lt;/em&gt;, but too
many &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s that either a) aren’t critical, or b) don’t get used at all,
is definitely wasteful.&lt;/p&gt;

&lt;p&gt;Flooding the network with unnecessary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s early in the page load
lifecycle can steal valuable bandwidth that could have been given to more
important resources—the overhead of certificates alone can exceed 3KB. Further,
opening and persisting connections has a CPU overhead on both the client and the
server. Lastly, Chrome will close a connection if it isn’t used within the first
10 seconds of being opened, so if you act too soon, you might end up doing it
all over again anyway.&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;, you should strive for &lt;strong&gt;as few as possible but as many as
necessary&lt;/strong&gt;. In fact, I would consider too many &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s a code smell, and
you probably ought to solve larger issues like &lt;a href=&quot;/2019/05/self-host-your-static-assets/&quot;&gt;self-hosting your static
assets&lt;/a&gt; and reducing reliance on third
parties in general.&lt;/p&gt;

&lt;h2 id=&quot;when-to-use-crossorigin&quot;&gt;When to Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Okay. Now it’s time to learn why the BBC’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s weren’t working!&lt;/p&gt;

&lt;p&gt;This is the third time I’ve seen this problem this month (and we’re only nine
days in…). It stems from a misunderstanding around when to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt;.
I get the impression that developers think ‘this request is going to another
origin, so it must need the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; attribute’. But that’s not what the
attribute is for—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; is used to define the CORS policy for the
request. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin=anonymous&lt;/code&gt; (or a bare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; attribute) will never
exchange any user credentials (e.g. cookies); &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin=use-credentials&lt;/code&gt; will
always exchange credentials. Unless you know that you need it, you almost never
need the latter. But when do we use the former?&lt;/p&gt;

&lt;p&gt;If the resulting request for a file would be CORS-enabled, you would need
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; on the corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;. Unfortunately, CORS isn’t the
most straightforward thing in the world. Fortunately, I have a shortcut…&lt;/p&gt;

&lt;p&gt;Firstly, identify a file on the origin that you’re considering &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;ing.
For example, let’s take a look at the BBC’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;box.css&lt;/code&gt;. In DevTools (or
WebPageTest if you already have one available—you don’t need to run one just for
this task), look at the resource’s &lt;strong&gt;request&lt;/strong&gt; headers:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/12/devtools-request-headers.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;783&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;There it is right there: &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sec-Fetch-Mode: no-cors&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nav.files.bbci.co.uk&lt;/code&gt; doesn’t &lt;em&gt;currently&lt;/em&gt; (I’ll
come back to that shortly) need a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; attribute:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://nav.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s look at another request. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orbit-v5-ltr.min.css&lt;/code&gt; from
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt; also carries a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sec-Fetch-Mode: no-cors&lt;/code&gt; request
header, so that won’t need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; either:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://nav.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://static.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s keep looking.&lt;/p&gt;

&lt;p&gt;How about the font &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BBCReithSans_W_Rg.woff2&lt;/code&gt; also from
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt;?&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/12/devtools-request-headers-02.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;783&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Hmm. This &lt;em&gt;does&lt;/em&gt; need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; as it’s marked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sec-Fetch-Mode: cors&lt;/code&gt;. What
do we do here?&lt;/p&gt;

&lt;p&gt;Simple!&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://nav.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://static.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://static.files.bbci.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We just add a second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; to open an additional CORS-enabled connection
to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt;. (Remember earlier when the browser had opened five
connections to four origins? One of them was CORS-enabled!)&lt;/p&gt;

&lt;p&gt;Let’s keep going and see where we end up…&lt;/p&gt;

&lt;p&gt;As it stands, the very specific example of the homepage right now, needs the
following &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s. Notice that all origins didn’t need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt;,
except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt; which needed both:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://nav.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://static.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://static.files.bbci.co.uk&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;crossorigin&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://m.files.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preconnect&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://ichef.bbci.co.uk&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This feels comfortable! The browser naturally opened five connections, so I’m
happy to see that we’ve also landed on five &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s; nothing is
unaccounted for.&lt;/p&gt;

&lt;h3 id=&quot;sec--request-headers&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sec-*&lt;/code&gt; Request Headers&lt;/h3&gt;

&lt;p&gt;I’d recommend familiarising yourself with the entire suite of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sec-*&lt;/code&gt;
headers—they’re incredibly useful debugging tools.&lt;/p&gt;

&lt;h2 id=&quot;preconnect-and-dns&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; and DNS&lt;/h2&gt;

&lt;p&gt;Because &lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt; is simply IP resolution, it is
unaffected by anything CORS-related. This means that:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;If you have mistakenly configured your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt;s&lt;/strong&gt; to use or omit
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt; when you should have actually omitted or used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crossorigin&lt;/code&gt;,
the &lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt; step can still be reused—only the
&lt;b style=&quot;color: #eb8a30&quot;&gt;TCP&lt;/b&gt; and &lt;b style=&quot;color: #c94bd4&quot;&gt;TLS&lt;/b&gt; need
discarding and doing again. That said, &lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt; is
usually—by far—the fastest part of the process anyway, so speeding it up
while missing out on &lt;b style=&quot;color: #eb8a30&quot;&gt;TCP&lt;/b&gt; and
&lt;b style=&quot;color: #c94bd4&quot;&gt;TLS&lt;/b&gt; isn’t much of an optimisation to celebrate.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;If you have everything configured correctly&lt;/strong&gt;, or you aren’t using
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preconnect&lt;/code&gt; at all, you’ll actually see the browser reusing the
&lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt; resolution for a subsequent request that
needs a different CORS mode. If you zoom right in on this abridged waterfall,
you’ll see that the second CORS-enabled request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static.files.bbci.co.uk&lt;/code&gt;
doesn’t incur any &lt;b style=&quot;color: #318a90&quot;&gt;DNS&lt;/b&gt; at all:
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/12/bbc-news-waterfall-dns.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;182&quot; loading=&quot;lazy&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
        <pubDate>Sat, 09 Dec 2023 19:17:04 +0000</pubDate>
        <link>https://csswizardry.com/2023/12/correctly-configure-preconnections/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/12/correctly-configure-preconnections/</guid>
      </item>
    
      <item>
        <title>The Three Cs: 🤝 Concatenate, 🗜️ Compress, 🗳️ Cache</title>
        <description>&lt;p&gt;I began writing this article in early July 2023 but began to feel a little
underwhelmed by it and so left it unfinished. However, after
&lt;a href=&quot;https://twitter.com/dhh/status/1712145950397841826&quot;&gt;recent&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/dhh/status/1719041666412347651&quot;&gt;renewed
discussions&lt;/a&gt; around the
relevance and usefulness of build steps, I decided to dust it off and get it
finished.&lt;/p&gt;

&lt;p&gt;Let’s go!&lt;/p&gt;

&lt;p&gt;When serving and storing files on the web, there are a number of different
things we need to take into consideration in order to balance ergonomics,
performance, and effectiveness. In this post, I’m going to break these processes
down into each of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;🤝 &lt;strong&gt;Concatenating&lt;/strong&gt; our files on the server: Are we going to send many
smaller files, or are we going to send one monolithic file? The former makes
for a simpler build step, but is it faster?&lt;/li&gt;
  &lt;li&gt;🗜️ &lt;strong&gt;Compressing&lt;/strong&gt; them over the network: Which compression algorithm, if
any, will we use? What is the availability, configurability, and efficacy of
each?&lt;/li&gt;
  &lt;li&gt;🗳️ &lt;strong&gt;Caching&lt;/strong&gt; them at the other end: How long should we cache files on
a user’s device? And do any of our previous decisions dictate our options?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;-concatenate&quot;&gt;🤝 Concatenate&lt;/h2&gt;

&lt;p&gt;Concatenation is probably the trickiest bit to get right because, even though
the three &lt;i&gt;C&lt;/i&gt;s happen in order, decisions we make later will influence
decisions we make back here. We need to think in both directions right now.&lt;/p&gt;

&lt;p&gt;Back in the HTTP/1.1 world, we were only able to fetch six resources at a time
from a given origin. Given this limitation, it was advantageous to have fewer
files: if we needed to download 18 files, that’s three separate chunks of work;
if we could somehow bring that number down to six, it’s only one discrete chunk
of work. This gave rise to heavy bundling and concatenation—why download three
CSS files (half of our budget) if we could compress them into one?&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Given that &lt;a href=&quot;https://almanac.httparchive.org/en/2022/http#fig-2&quot;&gt;66% of all
websites&lt;/a&gt; (and &lt;a href=&quot;https://almanac.httparchive.org/en/2022/http#fig-1&quot;&gt;77% of all
requests&lt;/a&gt;) are running
HTTP/2, I will not discuss concatenation strategies for HTTP/1.1 in this
article. If you &lt;em&gt;are&lt;/em&gt; still running HTTP/1.1, my only advice is to upgrade to
HTTP/2.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;With the introduction of HTTP/2, things changed. Instead of being limited to
only six parallel requests to a given origin, we were given the ability to open
a connection that could be reused infinitely. Suddenly, we could make far more
than six requests at a time, so bundling and concatenation became far less
relevant. An anti pattern, even.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or did it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It turns out &lt;a href=&quot;/2023/07/the-http1liness-of-http2/&quot;&gt;H/2 acts more like H/1.1 than you might
think…&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an experiment, I took the &lt;cite&gt;CSS Wizardry&lt;/cite&gt; homepage and crudely
added Bootstrap. In one test, I concatenated it all into one big file, and the
other had the library split into 12 files. I’m measuring when the last
stylesheet arrives&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, which is denoted by the vertical purple line. This will
be referred to as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;css_time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;a href=&quot;#appendix-test-methodology&quot;&gt;Read the complete test methodology.&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Plotted on the same horizontal axis of 1.6s, the waterfalls speak for
themselves:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/wp-content/uploads/2023/10/brotli-3g-one.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;131&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;201ms of cumulative latency; 109ms of cumulative download. &lt;a href=&quot;/wp-content/uploads/2023/10/brotli-3g-one.png&quot;&gt;(View full size.)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;With one huge file, we got a &lt;strong&gt;1,094ms &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;css_time&lt;/code&gt;&lt;/strong&gt; and transferred &lt;strong&gt;18.4KB of
CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/wp-content/uploads/2023/10/brotli-3g-many.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;318&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;4,362ms of cumulative latency; 240ms of cumulative download. &lt;a href=&quot;/wp-content/uploads/2023/10/brotli-3g-many.png&quot;&gt;(View full size.)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;With many small files, as ‘recommended’ in HTTP/2-world, we got &lt;strong&gt;a 1,524ms
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;css_time&lt;/code&gt;&lt;/strong&gt; and transferred &lt;strong&gt;60KB of CSS&lt;/strong&gt;. Put another way, the HTTP/2 way
was about &lt;strong&gt;1.4× slower&lt;/strong&gt; and about &lt;strong&gt;3.3× heavier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What might explain this phenomenon?&lt;/p&gt;

&lt;p&gt;When we talk about downloading files, we—generally speaking—have two things to
consider: latency and bandwidth. In the waterfall charts above, we notice we
have both light and dark green in the CSS responses: the light green can be
considered latency, while the dark green is when we’re actually downloading
data. As a rule, latency stays constant while download time is proportional to
filesize. Notice just how much more light green (especially compared to dark) we
see in the many-files version of Bootstrap compared to the one-big-file.&lt;/p&gt;

&lt;p&gt;This is not a new phenomenon—a client of mine suffered &lt;a href=&quot;/2023/07/in-defence-of-domcontentloaded/#putting-it-to-use&quot;&gt;the same problem in
July&lt;/a&gt;, and the Khan
Academy ran into &lt;a href=&quot;https://blog.khanacademy.org/forgo-js-packaging-not-so-fast/&quot;&gt;the same
issue&lt;/a&gt; in 2015!&lt;/p&gt;

&lt;p&gt;If we take some very simple figures, we can soon model the point with numbers…&lt;/p&gt;

&lt;p&gt;Say we have one file that takes &lt;strong&gt;1,000ms to download&lt;/strong&gt; with &lt;strong&gt;100ms of
latency&lt;/strong&gt;. Downloading this one file takes:&lt;/p&gt;

&lt;p&gt;(1 × 1000ms) + (1 × 100ms) = &lt;strong&gt;1,100ms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s say we chunk that file into 10 files, thus 10 requests each taking
a tenth of a second, now we have:&lt;/p&gt;

&lt;p&gt;(10 × 100ms) + (10 × 100ms) = &lt;strong&gt;2,000ms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because we added ‘nine more instances of latency’, we’ve pushed the overall time
from 1.1s to 2s.&lt;/p&gt;

&lt;p&gt;In our specific examples above, the one-big-file pattern incurred 201ms of
latency, whereas the many-files approach accumulated 4,362ms by comparison.
That’s almost 22× more!&lt;/p&gt;

&lt;p&gt;&lt;small&gt;It’s worth noting that, for the most part, the increase is parallelised,
so while it amounts to 22× more overall latency, it wasn’t back-to-back.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;It gets worse. As compression favours larger files, the overall size of the 10
smaller files will be greater than the original one file. Add to that &lt;a href=&quot;/2023/07/the-http1liness-of-http2/&quot;&gt;the
browser’s scheduling mechanisms&lt;/a&gt;, we’re
unlikely to dispatch all 10 requests at the same time.&lt;/p&gt;

&lt;p&gt;So, it looks like one huge file is the fastest option, right? What more do we
need to know? We should just bundle everything into one, no?&lt;/p&gt;

&lt;p&gt;As I said before, we have a few more things to juggle all at once here. We need
to learn a little bit more about the rest of our setup before we can make
a final decision about our concatenation strategy.&lt;/p&gt;

&lt;h2 id=&quot;️-compress&quot;&gt;🗜️ Compress&lt;/h2&gt;

&lt;p&gt;The above tests were run with Brotli compression&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. What happens when we
adjust our compression strategy?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1PKedBijfkrV1Y6gbzi71Ozw5ylBnq2EZLlAt2lfAEUk/edit#gid=340656194&quot;&gt;As of
2022&lt;/a&gt;,
roughly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;28% of compressible responses were Brotli encoded;&lt;/li&gt;
  &lt;li&gt;46% were Gzipped;&lt;/li&gt;
  &lt;li&gt;25% were, worryingly, not compressed at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What might each of these approaches mean for us?&lt;/p&gt;

&lt;figure&gt;
&lt;table&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Compression&lt;/th&gt;
    &lt;th&gt;Bundling&lt;/th&gt;
    &lt;th style=&quot;text-align: right;&quot;&gt;&lt;code&gt;css_time&lt;/code&gt; (ms)&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;
    &lt;th rowspan=&quot;2&quot;&gt;None&lt;/th&gt;
    &lt;td&gt;One file&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #9f102e;&quot;&gt;4,204&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Many files&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #3f990f;&quot;&gt;3,663&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th rowspan=&quot;2&quot;&gt;Gzip&lt;/th&gt;
    &lt;td&gt;One file&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #3f990f;&quot;&gt;1,190&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Many files&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #9f102e;&quot;&gt;1,485&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th rowspan=&quot;2&quot;&gt;Brotli&lt;/th&gt;
    &lt;td&gt;One file&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #3f990f;&quot;&gt;1,094&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Many files&lt;/td&gt;
    &lt;td style=&quot;text-align: right; color: #9f102e;&quot;&gt;1,524&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;If you can’t compress your files, splitting them out is
faster.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Viewed a little more visually:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/wp-content/uploads/2023/10/chart-compression.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;710&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;&lt;a href=&quot;/wp-content/uploads/2023/10/chart-compression.png&quot;&gt;(View full
size.)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;These numbers tell us that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;at &lt;strong&gt;low (or no) compression&lt;/strong&gt;, many smaller files is faster than one large
one;&lt;/li&gt;
  &lt;li&gt;at &lt;strong&gt;medium compression&lt;/strong&gt;, one large file is marginally faster than many
smaller
ones;&lt;/li&gt;
  &lt;li&gt;at &lt;strong&gt;higher compression&lt;/strong&gt;, one large file is markedly faster than many smaller
ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically, the more aggressive your ability to compress, the better you’ll fare
with larger files. This is because, at present, algorithms like Gzip and Brotli
become more effective the more historical data they have to play with. In other
words, larger files compress more than smaller ones.&lt;/p&gt;

&lt;p&gt;This shows us the sheer power and importance of compression, so ensure you have
the best setup possible for your infrastructure. If you’re not currently
compressing your text assets, that is a bug and needs addressing. Don’t optimise
to a sub-optimal scenario.&lt;/p&gt;

&lt;p&gt;This looks like another point in favour of serving one-big-file, right?&lt;/p&gt;

&lt;h2 id=&quot;️-cache&quot;&gt;🗳️ Cache&lt;/h2&gt;

&lt;p&gt;Caching is something I’ve been &lt;a href=&quot;https://speakerdeck.com/csswizardry/cache-rules-everything&quot;&gt;obsessed with
lately&lt;/a&gt;, but for the
static assets we’re discussing today, we don’t need to know much other than:
cache everything as aggressively as possible.&lt;/p&gt;

&lt;p&gt;Each of your bundles &lt;strong&gt;requires&lt;/strong&gt; a unique fingerprint, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.af8a22.css&lt;/code&gt;.
Once you’ve done this, caching is a simple case of storing the file forever,
immutably:&lt;/p&gt;

&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;Cache-Control: max-age=2147483648, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age=2147483648&lt;/code&gt;:&lt;/strong&gt; &lt;a href=&quot;/2019/03/cache-control-for-civilians/#max-age&quot;&gt;This
directive&lt;/a&gt; instructs all caches
to store the response for the maximum possible time. &lt;small&gt;We’re all used to
seeing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age=31536000&lt;/code&gt;, which is one year. This is perfectly reasonable and
practical for almost any static content, but if the file really is immutable,
we might as well shoot for forever. In the 32-bit world, forever is
&lt;a href=&quot;/2023/10/what-is-the-maximum-max-age/&quot;&gt;2,147,483,648&lt;/a&gt; seconds, or 68
years.&lt;/small&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;immutable&lt;/code&gt;:&lt;/strong&gt; &lt;a href=&quot;/2019/03/cache-control-for-civilians/#immutable&quot;&gt;This
directive&lt;/a&gt; instructs caches
that the file’s content will &lt;em&gt;never&lt;/em&gt; change, and therefore to never bother
revalidating the file once its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt; is met. You can &lt;em&gt;only&lt;/em&gt; add this
directive to responses that are fingerprinted (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.af8a22.css&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All static assets—provided they &lt;em&gt;are&lt;/em&gt; fingerprinted—can safely carry such an
aggressive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt; header as they’re very easy to cache bust. Which
brings me nicely on to…&lt;/p&gt;

&lt;p&gt;The important part of this section is cache &lt;em&gt;busting&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We’ve seen how heavily-&lt;strong&gt;concatenated&lt;/strong&gt; files &lt;strong&gt;compress&lt;/strong&gt; better, thus download
faster, but how does that affect our &lt;strong&gt;caching&lt;/strong&gt; strategy?&lt;/p&gt;

&lt;p&gt;While monolithic bundles might be faster overall for first-time visits, they
suffer one huge downfall: even a tiny, one-character change to the bundle would
require that a user redownload the entire file just to access one trivial
change. Imagine having to fetch a several-hundred kilobyte CSS file all over
again for the sake of changing one hex code:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  .c-btn {
&lt;span class=&quot;gd&quot;&gt;-   background-color: #C0FFEE;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+   background-color: #BADA55;
&lt;/span&gt;  }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is the risk with monolithic bundles: discrete updates can carry a lot of
redundancy. This is further exacerbated if you release very frequently: while
caching for 68 years and releasing 10+ times a day is perfectly safe, it’s a lot
of churn, and we don’t want to retransmit the same unchanged bytes over and over
again.&lt;/p&gt;

&lt;p&gt;Therefore, the most effective bundling strategy would err on the side of
as few bundles as possible to make the most of compression and scheduling, but
enough bundles to split out high- and low-rate of change parts of your codebase
so as to hit the most optimum caching strategy. It’s a balancing act for sure.&lt;/p&gt;

&lt;h2 id=&quot;-connection&quot;&gt;📡 Connection&lt;/h2&gt;

&lt;p&gt;One thing we haven’t looked at is the impact of network speeds on these
outcomes. Let’s introduce a fourth &lt;em&gt;C&lt;/em&gt;—&lt;em&gt;Connection&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I ran all of the tests over the following connection types:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;3G:&lt;/strong&gt; 1.6 Mbps downlink, 768 Kbps uplink, 150ms RTT&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;4G:&lt;/strong&gt; 9 Mbps downlink, 9 Mbps uplink, 170ms RTT&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cable:&lt;/strong&gt; 5 Mbps downlink, 1 Mbps uplink, 28ms RTT&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fibre:&lt;/strong&gt; 20 Mbps downlink, 5 Mbps uplink, 4ms RTT&lt;/li&gt;
&lt;/ul&gt;

&lt;figure&gt;
  &lt;img src=&quot;/wp-content/uploads/2023/10/chart-all.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;704&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;All variants begin to converge on a similar timing as network speed
improves. &lt;a href=&quot;/wp-content/uploads/2023/10/chart-all.png&quot;&gt;(View full
size.)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This data shows us that:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;the difference between no-compression and any compression is vast, especially
at slower connection speeds;
    &lt;ul&gt;
      &lt;li&gt;the helpfulness of compression decreases as connection speed increases;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;many smaller files is faster at all connection speeds if compression is
unavailable;&lt;/li&gt;
  &lt;li&gt;one big file is faster at all connection speeds as long as it is compressed;
    &lt;ul&gt;
      &lt;li&gt;one big file is only marginally faster than many small files over Gzip, but
faster nonetheless, and;&lt;/li&gt;
      &lt;li&gt;one big file over Brotli is markedly faster than many small files.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Again, no compression is not a viable option and should be considered
a bug—please don’t design your bundling strategy around the absence of
compression.&lt;/p&gt;

&lt;p&gt;This is another nod in the direction of preferring fewer, larger files.&lt;/p&gt;

&lt;h2 id=&quot;-client&quot;&gt;📱 Client&lt;/h2&gt;

&lt;p&gt;There’s a fifth &lt;em&gt;C&lt;/em&gt;! The &lt;em&gt;Client&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Everything we’ve looked at so far has concerned itself with network performance.
What about what happens in the browser?&lt;/p&gt;

&lt;p&gt;When we run JavaScript, we have three main steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Parse:&lt;/strong&gt; the browser parses the JavaScript to create an AST.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compile:&lt;/strong&gt; the parsed code is compiled into optimised bytecode.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Execute:&lt;/strong&gt; the code is now executed, and does whatever we wanted it to do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Larger files will inherently have higher parse and compile times, but aren’t
&lt;em&gt;necessarily&lt;/em&gt; slower to execute. It’s more about what your JavaScript is doing
rather than the size of the file itself: it’s possible to write a tiny file that
has a far higher runtime cost than a file a hundred times larger.&lt;/p&gt;

&lt;p&gt;The issue here is more about shipping an appropriate amount of code full-stop,
and less about how it’s bundled.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;As an example, I have a client with a 2.4MB main bundle (unfortunately
that isn’t a typo) which takes less than 10ms to compile on a mid-tier
mobile device.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;my-advice&quot;&gt;My Advice&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Ship as little as you can get away with in the first place.
    &lt;ul&gt;
      &lt;li&gt;It’s better to send no code than it is to compress 1MB down to 50KB.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;If you’re running HTTP/1.1, try upgrade to HTTP/2 or 3.&lt;/li&gt;
  &lt;li&gt;If you have no compression, get that fixed before you do anything else.&lt;/li&gt;
  &lt;li&gt;If you’re using Gzip, try upgrade to Brotli.&lt;/li&gt;
  &lt;li&gt;Once you’re on Brotli, it seems that larger files fare better over the
network.
    &lt;ul&gt;
      &lt;li&gt;Opt for fewer and larger bundles.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;The bundles you do end up with should, ideally, be based loosely on rate or
likelihood of change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have everything in place, then:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bundle infrequently-changing aspects of your app into fewer, larger bundles.&lt;/li&gt;
  &lt;li&gt;As you encounter components that appear less globally, or change more
frequently, begin splitting out into smaller files.&lt;/li&gt;
  &lt;li&gt;Fingerprint all of them and &lt;a href=&quot;/2023/10/what-is-the-maximum-max-age/&quot;&gt;cache them
forever&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Overall, err on the side of fewer bundles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;vendor.1a3f5b7d.js&lt;/span&gt;   &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;app.8e2c4a6f.js&lt;/span&gt;      &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;home.d6b9f0c7.js&lt;/span&gt;     &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;carousel.5fac239e.js&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vendor.js&lt;/code&gt;&lt;/strong&gt; is needed by every page and probably updates very
infrequently: we shouldn’t force users to redownload it any time we make
a change to any first-party JS.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.js&lt;/code&gt;&lt;/strong&gt; is also needed by every page but probably updates more often than
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vendor.js&lt;/code&gt;: we should probably cache these two separately.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;home.js&lt;/code&gt;&lt;/strong&gt; is only needed on the home page: there’s no point bundling it
into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.js&lt;/code&gt; which would be fetched on every page.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;carousel.js&lt;/code&gt;&lt;/strong&gt; might be needed a few pages, but not enough to warrant
bundling it into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.js&lt;/code&gt;: discrete changes to components shouldn’t require
fetching all of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.js&lt;/code&gt; again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-future-is-brighter&quot;&gt;The Future Is Brighter&lt;/h2&gt;

&lt;p&gt;The reason we’re erring on the side of fewer, larger bundles is that
currently-available compression algorithms work by compressing a file against
itself. The larger a file is, the more historical data there is to compress
subsequent chunks of the file against, and as compression favours repetition,
the chance of recurring phrases increases the larger the file gets. It’s kind of
self-fulfilling.&lt;/p&gt;

&lt;p&gt;Understanding why things work this way is easier to visualise with a simple
model. Below (and unless you want to count them, you’ll just have to believe
me), we have one-thousand &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; characters:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This takes up 1,000 bytes of data. We could represent these one-thousand &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt;s as
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1000(a)&lt;/code&gt;, which takes up just seven bytes of data, but can be multiplied back
out to restore the original thousand-character string with no loss of data. This
is lossless compression.&lt;/p&gt;

&lt;p&gt;If we were to split this string out into 10 files each containing 100 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt;s, we’d
only be able to store those as:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;100(a)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s ten lots of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;100(a)&lt;/code&gt;, which comes in at 60 bytes as opposed to the seven
bytes achieved with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1000(a)&lt;/code&gt;. While 60 is still much smaller than 1,000, it’s
much less effective than one large file as before.&lt;/p&gt;

&lt;p&gt;If we were to go even further, one-thousand files with a lone &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt;  character in
each, we’d find that things actually get larger! Look:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;harryroberts &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; ~/Sites/compression on &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
» &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-lhFG&lt;/span&gt;
total 15608
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;    1 harryroberts  staff   1.0K 23 Oct 09:29 1000a.txt
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;    1 harryroberts  staff    40B 23 Oct 09:29 1000a.txt.gz
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;    1 harryroberts  staff     2B 23 Oct 09:29 1a.txt
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;    1 harryroberts  staff    29B 23 Oct 09:29 1a.txt.gz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Attempting to compress a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; character &lt;em&gt;increases&lt;/em&gt; the file size from two
bytes to 29. One mega-file compresses from 1,000 bytes down to 40 bytes; the
same data across 1,000 files would cumulatively come in at 29,000 bytes—that’s
725 times larger.&lt;/p&gt;

&lt;p&gt;Although an extreme example, in the right (wrong?) circumstances, things can get
worse with many smaller bundles.&lt;/p&gt;

&lt;h3 id=&quot;shared-dictionary-compression-for-http&quot;&gt;Shared Dictionary Compression for HTTP&lt;/h3&gt;

&lt;p&gt;There was an attempt at compressing files against predefined, external
dictionaries so that even small files would have a much larger dataset available
to be compressed against. &lt;em&gt;Shared Dictionary Compression for HTTP&lt;/em&gt; (SDHC) was
pioneered by Google, and it worked by:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;…using pre-negotiated dictionaries to ‘warm up’ its internal state prior to
encoding or decoding. These may either be already stored locally, or uploaded
from a source and then cached.&lt;br /&gt;
— &lt;a href=&quot;https://en.wikipedia.org/wiki/SDCH&quot;&gt;SDHC&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately, SDHC was &lt;a href=&quot;https://chromestatus.com/feature/5763176272494592&quot;&gt;removed in Chrome 59 in
2017&lt;/a&gt;. Had it worked out,
we’d have been able to forgo bundling years ago.&lt;/p&gt;

&lt;h3 id=&quot;compression-dictionaries&quot;&gt;Compression Dictionaries&lt;/h3&gt;

&lt;p&gt;Friends &lt;a href=&quot;https://twitter.com/patmeenan&quot;&gt;Patrick Meenan&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/yoavweiss&quot;&gt;Yoav
Weiss&lt;/a&gt; have restarted work on implementing
an SDCH-like external dictionary mechanism, but with far more robust
implementation to avoid the issues encountered with previous attempts.&lt;/p&gt;

&lt;p&gt;While work is very much in its infancy, it is incredibly exciting. You can read
&lt;a href=&quot;https://github.com/WICG/compression-dictionary-transport&quot;&gt;the explainer&lt;/a&gt;, or
&lt;a href=&quot;https://datatracker.ietf.org/doc/draft-ietf-httpbis-compression-dictionary/00/&quot;&gt;the
Internet-Draft&lt;/a&gt;
already. We can &lt;a href=&quot;https://chromestatus.com/feature/5124977788977152&quot;&gt;expect Origin Trials
as we speak&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/WICG/compression-dictionary-transport/blob/main/examples.md&quot;&gt;early
outcomes&lt;/a&gt;
of this work show great promise, so this &lt;em&gt;is&lt;/em&gt; something to look forward to, but
widespread and ubiquitous support a way off yet…&lt;/p&gt;

&lt;h2 id=&quot;tldr&quot;&gt;tl;dr&lt;/h2&gt;

&lt;p&gt;In the current landscape, bundling is still a very effective strategy. Larger
files compress much more effectively and thus download faster at all connection
speeds. Further, queueing, scheduling, and latency work against us in
a many-file setup.&lt;/p&gt;

&lt;p&gt;However, one huge bundle would limit our ability to employ an effective caching
strategy, so begin to conservatively split out into bundles that are governed
largely by how often they’re likely to change. Avoid resending unchanged bytes.&lt;/p&gt;

&lt;p&gt;Future platform features will pave the way for simplified build steps, but even
the best compression in the world won’t sidestep the way HTTP’s scheduling
mechanisms work.&lt;/p&gt;

&lt;p&gt;Bundling is here to stay for a while.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;appendix-test-methodology&quot;&gt;Appendix: Test Methodology&lt;/h2&gt;

&lt;p&gt;To begin with, I as attempting to proxy the performance of each by taking the
&lt;em&gt;First Contentful Paint&lt;/em&gt; milestone. However, in the spirit of &lt;a href=&quot;/2022/08/measure-what-you-impact-not-what-you-influence/&quot;&gt;measuring what
I impact, not what
I influence&lt;/a&gt;,
I decided to lean on the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing&quot;&gt;User Timing
API&lt;/a&gt;
and drop a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;performance.mark()&lt;/code&gt; after
the last stylesheet:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;stylesheet&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;css_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;css_time&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can then pick this up in &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; using
their &lt;a href=&quot;https://docs.webpagetest.org/custom-metrics/&quot;&gt;Custom Metrics&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[css_time]
return css_time.startTime
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, I can append &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?medianMetric=css_time&lt;/code&gt; to the WebPageTest result URL and
automatically view &lt;a href=&quot;/2017/01/choosing-the-correct-average/&quot;&gt;the most
representative&lt;/a&gt;
of the test runs. You can also see this data in WebPageTest’s &lt;em&gt;Plot Full
Results&lt;/em&gt; view:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/wp-content/uploads/2023/10/wpt-full-results.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;681&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;For the one-big-file version, outliers were pushing 1.5s. &lt;a href=&quot;/wp-content/uploads/2023/10/wpt-full-results.png&quot;&gt;(View full size.)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;More or less. It’s accurate enough for this experiment. To be super-thorough, I should really grab the latest single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;responseEnd&lt;/code&gt; value of all of the CSS files, but we’d still arrive at the same conclusions. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;All compression modes were Cloudflare’s default settings and applied to all resources, including the host HTML document. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Tue, 17 Oct 2023 00:00:00 +0000</pubDate>
        <link>https://csswizardry.com/2023/10/the-three-c-concatenate-compress-cache/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/10/the-three-c-concatenate-compress-cache/</guid>
      </item>
    
      <item>
        <title>What Is the Maximum max-age?</title>
        <description>&lt;p&gt;If you wanted to cache a file ‘forever’, you’d probably use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt;
header like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Cache-Control: max-age=31536000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This instructs any cache that it may store and reuse a response for one year (60
seconds × 60 minutes × 24 hours × 365 days = &lt;strong&gt;31,536,000 seconds&lt;/strong&gt;). But why
one year? Why not 10 years? Why not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age=forever&lt;/code&gt;? Why not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age=∞&lt;/code&gt;?!&lt;/p&gt;

&lt;p&gt;I wondered the same. Let’s find out together.&lt;/p&gt;

&lt;details&gt;
  &lt;summary&gt;Like spoilers? See the answer.&lt;/summary&gt;
  &lt;p&gt;It’s &lt;code&gt;2147483648&lt;/code&gt; seconds, or 68 years. To find out why, read on!&lt;/p&gt;
&lt;/details&gt;

&lt;h2 id=&quot;max-age&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt; is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt; directive that instructs a cache that it may
store and reuse a response for &lt;var&gt;n&lt;/var&gt; seconds from the point at which it
entered the cache in question. Once that time has elapsed, the cache should
either revalidate the file with the origin server, or do whatever any
&lt;a href=&quot;/2019/03/cache-control-for-civilians/&quot;&gt;additional directives may have instructed it to
do&lt;/a&gt;. But why might we want to have
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt; that equates to forever?&lt;/p&gt;

&lt;h2 id=&quot;immutable&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;immutable&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;If we’re confident that we can cache a file for a year, we must be also quite
confident that it never &lt;em&gt;really&lt;/em&gt; changes. After all, a year is a very long time
in internet timescales. If we have this degree of confidence that a file won’t
change, we can cache the file &lt;i&gt;immutably&lt;/i&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;immutable&lt;/code&gt; is a &lt;a href=&quot;https://mailarchive.ietf.org/arch/msg/httpbisa/6gS9zGCh4tIB3hKa67wsoHdb4gY/&quot;&gt;relatively
new&lt;/a&gt;
directive that effectively &lt;a href=&quot;/2019/03/cache-control-for-civilians/#immutable&quot;&gt;makes a contract with the
browser&lt;/a&gt; in which we as
developers tell the browser: &lt;q&gt;this file will never, ever change, &lt;em&gt;ever&lt;/em&gt;;
please don’t bother coming back to the server to check for updates&lt;/q&gt;.&lt;/p&gt;

&lt;p&gt;Let’s say we have a simple source CSS file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.css&lt;/code&gt;. Its content is
as follows:&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;.c-btn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#C0FFEE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once our build system has completed, it will fingerprint the file and export it
with a unique hash, or &lt;em&gt;fingerprint&lt;/em&gt;, in its filename. The MD5 checksum for this
file is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7fda1016c4f1eaafc5a4e50a58308b79&lt;/code&gt;, so we’d probably end up with a file
named &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.7fda1016.css&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If we change the colour of the button, the next time we roll a release, the
build step will do its thing and now, the following content:&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;.c-btn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#BADA55&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;…would have a checksum of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6bb70b2a68a0e28913a05fb3656639b6&lt;/code&gt;. In that case, we’d
call the new file &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.6bb70b2a.css&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Notice how the content of the original file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.7fda1016.css&lt;/code&gt; hasn’t
changed; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.7fda1016.css&lt;/code&gt; has ceased to exist entirely, and is replaced by
a whole new file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button.6bb70b2a.css&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fingerprinted files never change—&lt;strong&gt;they get replaced&lt;/strong&gt;. This means we can safely
cache any fingerprinted file for, well, forever.&lt;/p&gt;

&lt;p&gt;But how long is forever?!&lt;/p&gt;

&lt;h2 id=&quot;31536000-seconds&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;31536000&lt;/code&gt; Seconds&lt;/h2&gt;

&lt;p&gt;Traditionally, developers have set ‘forever’ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt; values at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;31536000&lt;/code&gt;
seconds, which is a year. Why a year, though? A year isn’t forever. Was
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;31536000&lt;/code&gt; arrived at by agreement? Or is it specified somewhere? &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2616#section-14.21&quot;&gt;RFC
2616&lt;/a&gt; says of the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Expires&lt;/code&gt; header:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;To mark a response as “never expires,” an origin server sends an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Expires&lt;/code&gt;
date approximately one year from the time the response is sent. HTTP/1.1
servers SHOULD NOT send &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Expires&lt;/code&gt; dates more than one year in the future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Historically—&lt;em&gt;very&lt;/em&gt; historically—caching was bound to &lt;q&gt;approximately one year
from the time the response is sent&lt;/q&gt;. This restriction was introduced by the
long defunct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Expires&lt;/code&gt; header, and we’re talking about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt;, which is
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt; directive. Does &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt; say anything different?&lt;/p&gt;

&lt;h2 id=&quot;2147483648-seconds&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2147483648&lt;/code&gt; Seconds&lt;/h2&gt;

&lt;p&gt;It turns out there is a maximum value for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt;, and it’s defined in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9111#section-1.2.2&quot;&gt;RFC
9111’s
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delta-seconds&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A recipient parsing a delta-seconds value and converting it to binary form
ought to use an arithmetic type of at least 31 bits of non-negative integer
range. If a cache receives a delta-seconds value greater than the greatest
integer it can represent, or if any of its subsequent calculations overflows,
the cache &lt;strong&gt;MUST&lt;/strong&gt; consider the value to be 2147483648 (2&lt;sup&gt;31&lt;/sup&gt;) or the
greatest positive integer it can conveniently represent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The spec says caches should accept a maximum &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age&lt;/code&gt; value of
whatever-it’s-been-told, falling back to 2,147,483,648 seconds (which is 68
years), or failing that, falling back to as-long-as-it-possibly-can. This
wording means that, technically, there isn’t a maximum as long as the cache
understands the value you passed it. Theoretically, you could set
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-age=9999999999&lt;/code&gt; (that’s 317 years!) or higher. If the cache can work with
it, that’s how long it will store it. If it can’t handle 317 years, it should
fall back to 2,147,483,648 seconds, and if it can’t handle &lt;em&gt;that&lt;/em&gt;, whatever the
biggest value it can handle.&lt;/p&gt;

&lt;p&gt;And why 2,147,483,648 seconds?&lt;/p&gt;

&lt;p&gt;In a 32-bit system, the largest possible integer that can be represented in
binary form is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;01111111111111111111111111111111&lt;/code&gt;: a zero followed by 31 ones
(the first zero is reserved for switching between positive and negative values,
so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;11111111111111111111111111111111&lt;/code&gt; would be equal to -2,147,483,648).&lt;/p&gt;

&lt;h2 id=&quot;does-it-matter&quot;&gt;Does It Matter?&lt;/h2&gt;

&lt;p&gt;Honestly, no.&lt;/p&gt;

&lt;p&gt;It’s unlikely that a year would ever be insufficient, and it’s also unlikely
that any cache would store a file for that long anyway: browsers periodically
empty their cache as part of their general housekeeping, so even files that have
been stored for a year might not actually make it that long.&lt;/p&gt;

&lt;p&gt;This post was mostly an exercise in curiosity. But, if you wanted to, you could
go ahead and swap all of your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;31536000&lt;/code&gt;s for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2147483648&lt;/code&gt;s. It works in &lt;a href=&quot;https://cache-tests.fyi/&quot;&gt;all
major browsers&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Mon, 16 Oct 2023 14:18:39 +0000</pubDate>
        <link>https://csswizardry.com/2023/10/what-is-the-maximum-max-age/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/10/what-is-the-maximum-max-age/</guid>
      </item>
    
      <item>
        <title>How to Clear Cache and Cookies on a Customer’s Device</title>
        <description>&lt;p&gt;If you work in customer support for any kind of tech firm, you’re probably all
too used to talking people through the intricate, tedious steps of clearing
their cache and clearing their cookies. Well, there’s an easier way!&lt;/p&gt;

&lt;h2 id=&quot;getting-someone-to-clear-their-own-cache&quot;&gt;Getting Someone to Clear Their Own Cache&lt;/h2&gt;

&lt;p&gt;Trying to talk a non-technical customer through the steps of clearing their own
cache is not an easy task—not at all! From identifying their operating system,
platform, and browser, to trying to guide them—invisibly!—through different
screens, menus, and dropdowns is a big ask.&lt;/p&gt;

&lt;p&gt;Thankfully, any company that has folk in customer support can make use of a new
web platform feature to make the entire process a breeze: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;clear-site-data&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data&quot;&gt;A relatively new HTTP
header&lt;/a&gt;,
available in most modern browsers, allows developers to declaratively clear data
associated with a given origin&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; via one simple response header:
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;Clear-Site-Data: &quot;cache&quot;, &quot;cookies&quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Any response carrying this header will clear the caches&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; associated with that
&lt;em&gt;origin&lt;/em&gt;, so all your customer support team needs now is a simple URL that they
can send customers to that will clear all of their caches for them.&lt;/p&gt;

&lt;h3 id=&quot;preventing-malicious-clears&quot;&gt;Preventing Malicious Clears&lt;/h3&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/10/clear-site-data.png?1&quot; alt=&quot;Screenshot of a fictional webpage showing three buttons, labelled ‘Clear cache’, ‘Clear cookies’, and ‘Clear all’.&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;863&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;While it probably wouldn’t be disastrous, it is possible that a bad actor could
link someone directly to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://www.example.com/clear&lt;/code&gt; and force an
unsuspecting victim into clearing their cache or cookies.&lt;/p&gt;

&lt;p&gt;Instead, I would recommend that your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear&lt;/code&gt; page contains links to URLs like
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear/cache&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear/cookies&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear/all&lt;/code&gt;, each of which check and ensure
that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;referer&lt;/code&gt; request header is equal to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://www.example.com/clear&lt;/code&gt;.
This way, the only way the clearing works is if the user initiated it
themselves. Something maybe a little like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;referer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Referer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;referer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://www.example.com/clear&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Clear-Site-Data&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;403&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Forbidden: Invalid Referer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;clear-site-data-for-developers&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt; for Developers&lt;/h2&gt;

&lt;p class=&quot;c-highlight&quot;&gt;This isn’t the first time I’ve written about
&lt;code&gt;Clear-Site-Data&lt;/code&gt;—I mentioned it briefly in my 2019 article all about
&lt;a href=&quot;/2019/03/cache-control-for-civilians/#clear-site-data&quot;&gt;setting
the correct caching headers&lt;/a&gt;. However, this is the first time I’ve focused on
&lt;code&gt;Clear-Site-Data&lt;/code&gt; in its own right.&lt;/p&gt;

&lt;p&gt;Naturally, the use case isn’t just limited to customer support. As developers,
we may have messed something up and need to clear all visitors’ caches right
away. We could attach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt; header to all HTML responses for
a short period of time until we think the issue has passed.&lt;/p&gt;

&lt;p&gt;Note that this will prevent anything from going into cache while active, so you
will notice performance degradations. While ever the header is live, you will be
constantly evicting users’ caches, effectively disabling caching for your site
the whole time. Tread carefully!&lt;/p&gt;

&lt;h2 id=&quot;clearing-cache-on-ios&quot;&gt;Clearing Cache on iOS&lt;/h2&gt;

&lt;p&gt;Unfortunately, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clear-Site-Data&lt;/code&gt; is not supported by Safari, and as all browsers
on iOS are just Safari under the hood, there is no quick way to achieve this for
any of your iPhone users. Therefore, my advice to you is to immediately ask your
customer &lt;q&gt;Are you using an iPhone?&lt;/q&gt;. If the answer is no, direct them to
your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/clear&lt;/code&gt; page; if yes, then, well, I’m sorry. It’s back to the old
fashioned way.&lt;/p&gt;

&lt;p&gt;It’s also worth noting that Firefox doesn’t support the specific &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;cache&quot;&lt;/code&gt;
directive, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1671182&quot;&gt;it was removed in
94&lt;/a&gt;, but I can’t imagine
the average Firefox user would need assistance clearing their cache.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://www.bar.com&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://foo.bar.com&lt;/code&gt; are different origins: an origin is scoped to scheme, domain, and port. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://w3c.github.io/webappsec-Clear-Site-Data/#clear-cache&quot;&gt;The spec&lt;/a&gt; dictates that any sort of cache associated with the given origin should be cleared, and not just &lt;a href=&quot;/2019/03/cache-control-for-civilians/&quot;&gt;the HTTP cache&lt;/a&gt; &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Mon, 02 Oct 2023 15:30:49 +0000</pubDate>
        <link>https://csswizardry.com/2023/10/clear-cache-on-customer-device/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/10/clear-cache-on-customer-device/</guid>
      </item>
    
      <item>
        <title>The Ultimate Low-Quality Image Placeholder Technique</title>
        <description>&lt;p&gt;At the time of writing,
&lt;a href=&quot;https://almanac.httparchive.org/en/2022/media#images&quot;&gt;99.9%&lt;/a&gt; of pages on the
web include at least one image. The median image-weight per page landed at
&lt;a href=&quot;https://almanac.httparchive.org/en/2022/page-weight#fig-13&quot;&gt;881KB in 2022&lt;/a&gt;,
which is more than HTML, CSS, JS, and fonts combined! And while images do not
block rendering (unless you do &lt;a href=&quot;/2017/02/base64-encoding-and-performance/&quot;&gt;something
silly&lt;/a&gt;), it’s
important to consider how we offer a reasonably pleasant experience while users
are waiting for images to load. One solution to that problem is &lt;em&gt;Low-Quality
Image Placeholders&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;low-quality-image-placeholders&quot;&gt;Low-Quality Image Placeholders&lt;/h2&gt;

&lt;p&gt;Low-Quality Image Placeholders are nothing new. &lt;a href=&quot;https://twitter.com/guypod&quot;&gt;Guy
Podjarny&lt;/a&gt; is responsible, I &lt;em&gt;think&lt;/em&gt;, for &lt;a href=&quot;https://www.guypo.com/introducing-lqip-low-quality-image-placeholders&quot;&gt;coining
the term over a decade
ago&lt;/a&gt;! And
before that, we even had the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lowsrc&lt;/code&gt; attribute for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lowsrc=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;lo-res.jpg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;hi-res.jpg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;small&gt;I wish we’d never &lt;a href=&quot;https://html.spec.whatwg.org/multipage/obsolete.html#non-conforming-features&quot;&gt;deprecated
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lowsrc&lt;/code&gt;&lt;/a&gt;—it
would have saved us so much hassle in the long run.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The technique is simple: as images are typically heavier and slower resources,
and they don’t block rendering, we should attempt to give users something to
look at while they wait for the image to arrive. The solution? Show them
a low-quality image placeholder, or &lt;em&gt;LQIP&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The upshot is that the user knows that &lt;em&gt;something&lt;/em&gt; is happening, and, ideally,
they should have roughly some idea &lt;em&gt;what&lt;/em&gt; is happening—after all, we want our
LQIP to somewhat resemble the final image.&lt;/p&gt;

&lt;h2 id=&quot;core-web-vitals-and-largest-contentful-paint&quot;&gt;Core Web Vitals and Largest Contentful Paint&lt;/h2&gt;

&lt;p&gt;While LQIP isn’t a new subject at all, &lt;a href=&quot;/2023/07/core-web-vitals-for-search-engine-optimisation/&quot;&gt;Core Web
Vitals&lt;/a&gt;
and &lt;a href=&quot;/2022/03/optimising-largest-contentful-paint/&quot;&gt;Largest Contentful
Paint&lt;/a&gt;
are, and unfortunately, they don’t necessarily get along so well…&lt;/p&gt;

&lt;p&gt;If your LCP candidate is an image (whether that’s a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-image&lt;/code&gt; or an
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element), it’s going to be somewhat slower than if your LCP candidate
was a text node, and while &lt;a href=&quot;/2022/03/optimising-largest-contentful-paint/&quot;&gt;making image-based LCPs
fast&lt;/a&gt;
isn’t impossible, it is harder.&lt;/p&gt;

&lt;p&gt;Using an LQIP while we wait for our full-res LCP candidate certainly fills
a user-experience gap, but, owing to certain rules and restrictions with LCP as
a spec (more on that later), it’s unlikely to help our LCP scores.&lt;/p&gt;

&lt;p&gt;When the full resolution image eventually arrives, it’s likely that that image
will be counted as your LCP, and not your LQIP:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/separate-events.png&quot; width=&quot;1499&quot; height=&quot;383&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It would be nice to have a scenario whereby your LQIP &lt;em&gt;does&lt;/em&gt; meet requirements
for consideration as LCP, leading to sub-2.5s scores, but also load in a high
resolution soon after, thus improving the user experience. The best of both
worlds:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/same-event.png&quot; width=&quot;1456&quot; height=&quot;383&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Is that even possible? Let’s find out…&lt;/p&gt;

&lt;h2 id=&quot;largest-contentful-paint-caveats&quot;&gt;Largest Contentful Paint Caveats&lt;/h2&gt;

&lt;p&gt;There is some important nuance that we should be aware of before we go any
further. There are quite a few moving parts when it comes to how and when your
LCP candidates are captured, when they’re updated, and which candidate is
ultimately used.&lt;/p&gt;

&lt;p&gt;Chrome keeps taking new LCP candidates right up until a user interacts with the
page. This means that if an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; is visible immediately, a user scrolls, then
a larger &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; arrives moments after, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; is your LCP element for that
page. If a user doesn’t interact in that short window, a new entry is captured,
and now the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; is your LCP element. Notice below how our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; is
momentarily considered our LCP candidate at &lt;strong&gt;1.0s&lt;/strong&gt;, before ultimately being
replaced by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; at &lt;strong&gt;2.5s&lt;/strong&gt;:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-h1.png&quot; width=&quot;1500&quot; height=&quot;284&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Blue shading shows an LCP candidate; green shading and/or a red
border shows the actual LCP element and event. &lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-h1.png&quot;&gt;(View full
size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The key takeaway here is that &lt;strong&gt;Chrome keeps looking for new LCP candidates&lt;/strong&gt;,
and the moment it finds anything larger, it uses that.&lt;/p&gt;

&lt;p&gt;What if Chrome finds a later element of the &lt;em&gt;same&lt;/em&gt; size? Thankfully, Chrome will
not consider new elements of the same size as the previously reported LCP
candidate. That protects us in situations like this:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-grid.png&quot; width=&quot;1500&quot; height=&quot;284&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Things like image grids are measured on their first image, not their
last. This is great news. &lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-grid.png&quot;&gt;(View full
size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Note that at &lt;strong&gt;1.4s&lt;/strong&gt; we get our LCP event in full. When the other eight images
arrive at &lt;strong&gt;2.0s&lt;/strong&gt;, they make no difference to our score.&lt;/p&gt;

&lt;p&gt;This all seems straightforward enough—Chrome keeps on looking for the largest
element and then uses that, right? And it doesn’t necessarily spell bad news for
our LQIP either. As long as our final image is the same dimensions as the LQIP
was…?&lt;/p&gt;

&lt;p&gt;Not quite. There’s some subtle complexity designed to prevent people gaming the
system, which is exactly what we’re trying to do.&lt;/p&gt;

&lt;p class=&quot;c-highlight&quot;&gt;&lt;strong&gt;Warning:&lt;/strong&gt; It is imperative that you still
provide a great user experience. Passing LCP for metrics’ sake is unwise and
against the spirit of web performance. Ensure that your LQIP is still of
sufficient quality to be useful, and follow it up immediately with your
full-quality image. Poor quality images, particularly where ecommerce is
concerned, are &lt;a href=&quot;https://www.businesswire.com/news/home/20230517005168/en/Cloudinary-Global-E-Commerce-Survey-Reveals-Visual-Content-Can-Help-Reduce-Returns-by-One-Third&quot;&gt;especially
harmful&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;dont-upscale-your-lqip&quot;&gt;Don’t Upscale Your LQIP&lt;/h2&gt;

&lt;p&gt;Each image in the tests so far has been a 200×200px &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; displayed at
200×200px:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://dummyimage.com/200/000/fff.png?2&amp;amp;text=200@200&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Which is this, coming at 2KB:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dummyimage.com/200/000/fff.png?2&amp;amp;text=200@200&quot; width=&quot;200&quot; height=&quot;200&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What if we change the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; to 100×100px displayed at 200×200px, or
&lt;em&gt;upscaled&lt;/em&gt;?&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://dummyimage.com/100/000/fff.png?4&amp;amp;text=100@200&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Which comes in at 1.4KB:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dummyimage.com/100/000/fff.png?4&amp;amp;text=100@200&quot; width=&quot;200&quot; height=&quot;200&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Already, you can see the loss in quality associated with upscaling this
image.&lt;/small&gt;&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-upscaled.png&quot; width=&quot;1500&quot; height=&quot;284&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Upscaled images will be discounted against higher-resolution ones. &lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-upscaled.png&quot;&gt;(View full size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Above, we see that we log a candidate at &lt;strong&gt;1.5s&lt;/strong&gt;, but the second image at
&lt;strong&gt;2.0s&lt;/strong&gt; becomes our LCP despite being rendered at the exact same size!&lt;/p&gt;

&lt;p&gt;And there is the nuance. Chrome doesn’t want to reward a poor experience, so
simply serving a tiny image and displaying it much larger will not help your LCP
scores if a denser image turns up later on. And I agree with this decision, for
the most part.&lt;/p&gt;

&lt;p&gt;The first takeaway is: &lt;strong&gt;don’t upscale your LQIP&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;calculating-the-upscaling-penalty&quot;&gt;Calculating the Upscaling Penalty&lt;/h3&gt;

&lt;p&gt;Let’s get a bit more detailed about upscaling and penalties. Some &lt;a href=&quot;https://www.w3.org/TR/largest-contentful-paint/#sec-add-lcp-entry&quot;&gt;close reading
of the spec&lt;/a&gt;
tells us exactly how this works. It’s not the easiest thing to digest, but I’ll
do my best to distil it for you here. The reported &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;area&lt;/code&gt; of your LCP element is
calculated as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;area = size × penaltyFactor&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;size&lt;/code&gt;&lt;/strong&gt; is the area of the LCP candidate currently in the viewport and not
cropped or off-screen.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;penaltyFactor&lt;/code&gt;&lt;/strong&gt; is the factor by which upscaling will count against us,
given by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min(displaySize, naturalSize) / displaySize&lt;/code&gt;, where:
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;naturalSize&lt;/code&gt;&lt;/strong&gt; is the pixel area of the image file in question.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;displaySize&lt;/code&gt;&lt;/strong&gt; is the pixel area that the image will be rendered,
 regardless of how much of it is currently on-screen.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In full:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;area = size × min(displaySize, naturalSize) / displaySize&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Imagine we took a large landscape image, downscaled it to a predetermined
height, and then displayed it, cropped, as a square:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-spec.png&quot; width=&quot;1500&quot; height=&quot;856&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;In the above diagram, &lt;em&gt;a&lt;/em&gt; is &lt;code&gt;naturalSize&lt;/code&gt;, &lt;em&gt;b&lt;/em&gt; is &lt;code&gt;displaySize&lt;/code&gt;, and &lt;em&gt;c&lt;/em&gt; is
&lt;code&gt;size&lt;/code&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;For the sake of ease, let’s assume your LCP candidate is always fully on-screen,
in-viewport, and not cropped (if you have known and predictable cropping or
off-screen image data, you can adjust your maths accordingly). This means that
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;size&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;displaySize&lt;/code&gt; are now synonymous.&lt;/p&gt;

&lt;p&gt;Let’s say we have a 400×400px image that is &lt;strong&gt;downscaled&lt;/strong&gt; to 200×200px. Its
area would be calculated as:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200 × 200 × min(200 × 200, 400 × 400) / (200 × 200) = &lt;/code&gt; &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;40,000&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thus the LCP’s reported size would be 40,000px&lt;sup&gt;2&lt;/sup&gt;:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-devtools-upscaled-01.png&quot; width=&quot;1500&quot; height=&quot;887&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-devtools-upscaled-01.png&quot;&gt;(View full size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If we were to use a 100×100px image and &lt;strong&gt;upscale&lt;/strong&gt; it to 200×200px, our
equation looks a little different:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200 × 200 × min(200 × 200, 100 × 100) / (200 × 200) = &lt;/code&gt; &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10,000&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-devtools-upscaled-02.png&quot; width=&quot;1500&quot; height=&quot;887&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lcp-devtools-upscaled-02.png&quot;&gt;(View full size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This image’s reported area is significantly smaller, despite being rendered at
the exact same size! This means that any subsequent images of a higher quality
may well steal our LCP score away from this one and to a much later time.&lt;/p&gt;

&lt;p&gt;Even if we used a 199×199px LQIP, we’d still register a new LCP the moment our
full quality image arrives:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200 × 200 × min(200 × 200, 199 × 199) / (200 × 200) = &lt;/code&gt; &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;39,601&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That all got pretty academic, but my advice is basically: &lt;strong&gt;if you want your
LQIP to be considered as your LCP, do not upscale it.&lt;/strong&gt; If you do upscale it,
your reported area will come back smaller than you might expect, and thus the
later, high resolution image is likely to ‘steal’ the LCP score.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;strong&gt;N.B.&lt;/strong&gt; Thankfully, none of the specs take device pixels or pixel
densities into account. It’s CSS pixels all the way down.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;aim-for-a-minimum-of-005bpp&quot;&gt;Aim for a Minimum of 0.05BPP&lt;/h2&gt;

&lt;p&gt;The second restriction we need to get around is the &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/2023_04_lcp.md&quot;&gt;recently
announced&lt;/a&gt;
bits per pixel (BPP) threshold. Again, to stop people gaming the system, Chrome
decided that only images of a certain quality (or &lt;em&gt;entropy&lt;/em&gt;) will be considered
as your LCP element. This prevents people using incredibly low quality images in
order to register a fast LCP time:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;That heuristic discounts paints which are not contentful, but just serve as
backgrounds or placeholders for other content.&lt;/p&gt;

  &lt;p&gt;This change extends that heuristic to other images as well, when those images
have very little content, when compared to the size at which they are
displayed.&lt;br /&gt;
— &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/2023_04_lcp.md&quot;&gt;Largest Contentful Paint change in Chrome 112 to ignore low-entropy images&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one is much simpler to make sense of. In order for an image to be counted
as an LCP candidate, it needs to contain at least 0.05 bits of data per pixel
displayed.&lt;/p&gt;

&lt;p&gt;Note that this applies to the image’s displayed size and not its natural size:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Controls whether LCP calculations should exclude low-entropy images. If
enabled, then the associated parameter sets the cutoff, expressed as the
minimum number of bits of encoded image data used to encode each rendered
pixel. &lt;strong&gt;Note that this is not just pixels of decoded image data; the rendered
size includes any scaling applied by the rendering engine to display the
content.&lt;/strong&gt;&lt;br /&gt;
— &lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/common/features.cc;l=749&quot;&gt;features.cc&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A 200×200px image has 40,000 pixels. If we need 0.05 bits of data for each
pixel, the image needs to be at least 2,000 bits in size. To get that figure in
Kilobytes, we simply need to divide it by 8,000: 0.25KB. That’s tiny!&lt;/p&gt;

&lt;p&gt;A 1024×768px image?&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(1024 × 768 × 0.05) / 8000 =&lt;/code&gt; &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.9152KB&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;720×360px?&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(720 × 360 × 0.05) / 8000 =&lt;/code&gt; &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.62KB&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was much less academic, but my advice is basically: &lt;strong&gt;if you want your
LQIP to ever be considered as your LCP, make sure it contains enough data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To err on the side of caution, I go by a BPP figure of 0.055. Honestly, the
filesizes you’re aiming for are so small at this point that you’ll probably
struggle to get as low as 0.055BPP anyway, but it just seems wise to build in
10% of buffer in case any intermediaries attempt to compress your images
further. &lt;small&gt;(This should actually be impossible because you’re serving your
images over HTTPS, right?)&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;lqip-and-bpp-calculator&quot;&gt;LQIP and BPP Calculator&lt;/h2&gt;

&lt;p&gt;That’s a lot of specs and numbers. Let’s try make it all a little easier. I’ve
built this simplified calculator to help you work out the mathematically
smallest possible LCP candidate. It is this image that becomes your LQIP.&lt;/p&gt;

&lt;style&gt;

[id=jsInputWidth],
[id=jsInputHeight] {
   background: none;
   border: none;
   border-bottom: 1px solid #f43059;
   text-align: right;
   font-weight: bold;
   font-family: &quot;Operator Mono&quot;, Inconsolata, Monaco, Consolas, &quot;Andale Mono&quot;, &quot;Bitstream Vera Sans Mono&quot;, &quot;Courier New&quot;, Courier, monospace;
  -moz-appearance: textfield;
}

[id=jsForm] ::-webkit-outer-spin-button,
[id=jsForm] ::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

&lt;/style&gt;

&lt;form id=&quot;jsForm&quot;&gt;

&lt;p&gt;
    My image will be displayed at a maximum of
    &lt;input type=&quot;number&quot; id=&quot;jsInputWidth&quot; min=&quot;0&quot; max=&quot;10000&quot; value=&quot;1440&quot; /&gt;px wide and
    &lt;input type=&quot;number&quot; id=&quot;jsInputHeight&quot; min=&quot;0&quot; max=&quot;10000&quot; value=&quot;810&quot; /&gt;px high.
&lt;/p&gt;

&lt;/form&gt;

&lt;p id=&quot;jsOutput&quot; class=&quot;c-highlight&quot;&gt;Your LQIP should be &lt;strong id=&quot;jsOutputWidth&quot;&gt;1,440&lt;/strong&gt;×&lt;strong id=&quot;jsOutputHeight&quot;&gt;810&lt;/strong&gt;px
(&lt;strong id=&quot;jsArea&quot;&gt;1,166,400px&lt;sup&gt;2&lt;/sup&gt;&lt;/strong&gt;), and should have
a filesize no smaller than &lt;strong id=&quot;jsFilesize&quot;&gt;8.019KB&lt;/strong&gt;.&lt;/p&gt;

&lt;script&gt;
  const form         = document.getElementById('jsForm')
  const fileSize     = document.getElementById(&quot;jsFilesize&quot;);
  const output       = document.getElementById('jsOutput');
  const outputWidth  = document.getElementById('jsOutputWidth');
  const outputHeight = document.getElementById('jsOutputHeight');

  (update) = () =&gt; {

    const width  = 1 * (document.getElementById('jsInputWidth').value);
    const height = 1 * (document.getElementById('jsInputHeight').value);

    const calculatedFilesize  = (width * height * 0.055) / 8000;

    if (width &gt;= 0 &amp;&amp; typeof width === &quot;number&quot; &amp;&amp; height &gt;= 0 &amp;&amp; typeof height === &quot;number&quot;) {

      output.style.visibility = 'visible'
      jsArea.innerHTML = (width * height).toLocaleString() + 'px&lt;sup&gt;2&lt;/sup&gt;';
      outputWidth.innerHTML = width.toLocaleString();
      outputHeight.innerHTML = height.toLocaleString();
      fileSize.innerHTML = calculatedFilesize.toLocaleString() + 'KB';

    }

  }

  form.addEventListener('input', update);

&lt;/script&gt;

&lt;p&gt;Using the exact same calculator you’re playing with right now, I plugged in &lt;a href=&quot;/&quot;&gt;my
homepage’s&lt;/a&gt; numbers and rebuilt my LCP. I managed to get my LQIP–LCP down to
just &lt;strong&gt;1.1s&lt;/strong&gt; on a 3G connection.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/csswizardry.com-lcp.jpg&quot; width=&quot;1500&quot; height=&quot;819&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Note that my &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and a &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;
are initially flagged as candidates before Chrome finally settles on the image. &lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/csswizardry.com-lcp.jpg&quot;&gt;(View
full size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And from a cold cache, over train wifi as I was writing this post, I got a 2.1s
LCP score on desktop!&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/train-lcp.png&quot; width=&quot;1500&quot; height=&quot;887&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/train-lcp.png&quot;&gt;(View full
size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;implementing-low-quality-image-placeholders&quot;&gt;Implementing Low-Quality Image Placeholders&lt;/h2&gt;

&lt;p&gt;My implementation becomes incredibly simple as I’m using a background image.
This means I can simply layer up the progressively higher-resolution files using
CSS’ multiple backgrounds:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  ...

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preload&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;as=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;image&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;lo-res.jpg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;fetchpriority=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;high&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  ...

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;header&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;background-image: url(hi-res.jpg),
                                   url(lo-res.jpg)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ol&gt;
  &lt;li&gt;As &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-image&lt;/code&gt; is &lt;a href=&quot;/2022/03/optimising-largest-contentful-paint/#background-image-url&quot;&gt;hidden from the preload
scanner&lt;/a&gt;,
I’m &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preload&lt;/code&gt;ing the LQIP (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo-res.jpg&lt;/code&gt;) so that it’s already on its way
before the parser encounters the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;header&amp;gt;&lt;/code&gt;.
    &lt;ul&gt;
      &lt;li&gt;Note that I’m not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preload&lt;/code&gt;ing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hi-res.jpg&lt;/code&gt;—we don’t want the two images to
race each other, we want them to arrive one after the other.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Once the parser reaches the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, the request for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hi-res.jpg&lt;/code&gt; is
dispatched.
    &lt;ul&gt;
      &lt;li&gt;At this point, if it’s fully fetched, we can render &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo-res.jpg&lt;/code&gt; as the
 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;header&amp;gt;&lt;/code&gt;’s background.&lt;/li&gt;
      &lt;li&gt;If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo-res.jpg&lt;/code&gt; isn’t ready yet, we’d fall back to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt; or
 similar while we wait.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;As &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo-res.jpg&lt;/code&gt; is guaranteed to arrive first (it was requested much earlier
and is much smaller in file-size), it gets displayed first.&lt;/li&gt;
  &lt;li&gt;Once &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hi-res.jpg&lt;/code&gt; arrives, whenever that may be, it takes the place of
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lo-res.jpg&lt;/code&gt;, switching out automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/csswizardry/csswizardry.github.com/blob/5f0174b35bbb4cb7761d783291f0fdda3323521b/css/isolated/components.page-head--masthead.scss&quot;&gt;My &lt;em&gt;very specific&lt;/em&gt;
implementation&lt;/a&gt;
is more complex and nuanced than that (it’s responsive and I also use a super
low-resolution Base64 placeholder that’s far too small to be considered an LCP
candidate), but that’s the main technique in a few lines. My layers look like
this:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lqip-lcp.gif&quot; width=&quot;1500&quot; height=&quot;400&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/09/lqip-lcp.gif&quot;&gt;(View full size)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ol&gt;
  &lt;li&gt;The very first frame is &lt;strong&gt;810 bytes&lt;/strong&gt; of 16×11px Base64 image, &lt;em&gt;far&lt;/em&gt; too
small to qualify for LCP, and &lt;em&gt;massively&lt;/em&gt; upscaled:
&lt;img src=&quot;data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQECAgICAgICAgICAgMDAwMDAwMDAwMBAQEBAQEBAgEBAgICAQICAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA//AABEIAAsAEAMBEQACEQEDEQH/xACAAAEBAQAAAAAAAAAAAAAAAAAICQoQAAAEBAUEAQUAAAAAAAAAAAMEBQYBAhMUBxIVFhcACAkkERgjJTdDAQADAQAAAAAAAAAAAAAAAAAHCAkGEQAABAUCAwcEAwAAAAAAAAABAgMEBhESExQFBwAVIwgWFyEiJDMxMkJDNEFR/9oADAMBAAIRAxEAPwDP12CsRLfmKpRpN7FZ6ok7kxPHIoiWbLlCyQvgFTI6tKWeSihwEUEcgspheMBBwYyyZBBYhTS5YRg6O3m4ei7fQrF0dEjLWmLNukNo7Vgmcz1c4uBQTTIqIpkdCc5VyFUOBDAcxREtEzV42T1KEND2/ibX1ojiNJu11ZyKTZqk2kc1XoUXE5TmKLg4lIAmEqQmrAhpgPD872MFxcADY5d2dyb4TRXcArubaCHq72buHpsQ7EQ5hxOvmicTyhscsfKlJRjMJBjAEwU88YzRm6KPZ47TCMd7RpljeO3ejbg6UgRu+W1Fskcj1e2UwvG+EmQBQVERLScEzFUAwGD7RMUtrtf0OM4AePIiiGLmcSaaUCulk0UnKKhlCVFVTstxEqJhAxSgsKavp8wH68Sm8SV79RRyjy/bUGpqHF+gafa7sb37YvvyvE1fLqume5b/AD/Kp0kz3H8MH9/Bu5xrWfexrmMr8Nnp8wpnh5ntsi3+yniWGx9zO1WjK/qdmij8v5df6p/FR536fxnwpPK9pXJTiteXaO9X9a095fFfOk1dp6x715Wz67eetmt6f2qXWm7OuD4R6lLkWdlpzxMvvHO0pbyp+0xaZ4tHTtXMnq08HN7id2HV3m96+aXLsnMnSaU7fSs/5X6JVVefH//Z&quot; width=&quot;16&quot; height=&quot;11&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
  &lt;li&gt;The second frame is a &lt;a href=&quot;/img/css/masthead-large-lqip.jpg&quot;&gt;&lt;strong&gt;24KB&lt;/strong&gt;
image&lt;/a&gt; that is both
my LQIP &lt;em&gt;and&lt;/em&gt; my LCP.&lt;/li&gt;
  &lt;li&gt;The third and final frame is the full-resolution, &lt;a href=&quot;/img/css/masthead-large.jpg&quot;&gt;&lt;strong&gt;160KB&lt;/strong&gt;
image&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-image&lt;/code&gt; method only works if images are decorational. If your
image &lt;em&gt;is&lt;/em&gt; content (e.g. it’s a product image), then semantically,
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-image&lt;/code&gt; won’t be good enough. In this case, you’ll probably end up
absolutely positioning some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements, but it’s also worth noting that
you can apply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-image&lt;/code&gt;s to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt;s, so the technique I use will be
more or less identical. Something like this:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  ...

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;preload&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;as=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;image&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;lo-res.jpg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;fetchpriority=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;high&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  ...

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;hi-res.jpg&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;alt=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Appropriate alternate text&quot;&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;360&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;360&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;background-image: url(lo-res.jpg)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In fact, I do exactly that with &lt;a href=&quot;#section:sub-content&quot;&gt;the photo of me in the
sidebar&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;use-an-image-transformation-service&quot;&gt;Use an Image Transformation Service&lt;/h3&gt;

&lt;p&gt;Being so tightly bound to these figures isn’t very redesign-friendly—you’d have
to reprocess your entire image library if you made your LCP candidate any
bigger. With this in mind, I wouldn’t recommend attempting this manually, or
batch-processing your entire back catalogue.&lt;/p&gt;

&lt;p&gt;Instead, use a service like &lt;a href=&quot;https://cloudinary.com/&quot;&gt;Cloudinary&lt;/a&gt; to size and
compress images on the fly. This way, you only need to redesign a handful of
components and let Cloudinary do the rest on demand. They make available &lt;a href=&quot;https://cloudinary.com/documentation/image_optimization#set_the_quality_when_delivering_an_image&quot;&gt;a
quality
parameter&lt;/a&gt;
that takes a number which is &lt;q&gt;a value between 1 (smallest file size possible)
and 100 (best visual quality)&lt;/q&gt;. E.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;q_80&lt;/code&gt;. Note that this number is not
a percentage.&lt;/p&gt;

&lt;p&gt;To get your BPP down to roughly 0.05, you’re going to want to experiment with
a &lt;em&gt;really&lt;/em&gt; small number. Play around with numerous different images from your
site to ensure whatever quality setting you choose doesn’t ever take you &lt;em&gt;below&lt;/em&gt;
0.05BPP.&lt;/p&gt;

&lt;h3 id=&quot;use-your-judgement&quot;&gt;Use Your Judgement&lt;/h3&gt;

&lt;p&gt;If you do manage to get your image all the way down to your target filesize,
there’s every chance it will be &lt;em&gt;too&lt;/em&gt; low quality to be visually acceptable,
even if it does satisfy LCP’s technical requirements.&lt;/p&gt;

&lt;p&gt;Here’s a current client’s product image compressed down to 4KB (their target was
actually 3.015KB, but even the most aggressive settings couldn’t get me all the
way):&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;/wp-content/uploads/2023/09/too-far.jpg&quot; width=&quot;760&quot; height=&quot;577&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;This is visually unacceptable as an LCP candidate, even though it ticks every
box in the spec. My advice here—and it’s very subjective—is that you shouldn’t
accept an LQIP–LCP that you wouldn’t be happy for a user to look at for any
period of time.&lt;/p&gt;

&lt;p&gt;In this particular instance, I bumped the quality up to 10, which came in at
12KB, was still super fast, but was visually much more acceptable.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;/wp-content/uploads/2023/09/just-right.jpg&quot; width=&quot;760&quot; height=&quot;577&quot; alt=&quot;&quot; loading=&quot;lazy&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;In their attempts to prevent people gaming the system, spec writers have had to
define exactly what that system is. Ironically, codifying these constraints
makes gaming the system so much easier, as long as you can be bothered to read
the specifications (which, luckily for you, I have).&lt;/p&gt;

&lt;p&gt;Largest Contentful Paint candidates are penalised for upscaling and also for low
entropy. By understanding how the upscaling algorithm works, and how to
calculate target filesizes from input dimensions, we can generate the smallest
possible legitimate LCP image which can be used as a low-quality placeholder
while we wait for our full-resolution image to arrive. The best of both worlds.&lt;/p&gt;
</description>
        <pubDate>Thu, 28 Sep 2023 18:59:20 +0000</pubDate>
        <link>https://csswizardry.com/2023/09/the-ultimate-lqip-lcp-technique/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/09/the-ultimate-lqip-lcp-technique/</guid>
      </item>
    
      <item>
        <title>Core Web Vitals for Search Engine Optimisation: What Do We Need to Know?</title>
        <description>&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;

&lt;p&gt;Stay updated by following &lt;a href=&quot;https://twitter.com/csswizardry/status/1683353820900761600&quot;&gt;this article’s Twitter
thread&lt;/a&gt;. I will post
amendments and updates there.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;ins datetime=&quot;2023-07-26&quot;&gt;26 July, 2023: &lt;a href=&quot;#ios-and-other-traffic-doesnt-count&quot;&gt;iOS (and Other) Traffic Doesn’t Count&lt;/a&gt;&lt;/ins&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;core-web-vitals&quot;&gt;Core Web Vitals&lt;/h2&gt;

&lt;p&gt;Google’s Core Web Vitals initiative was launched in &lt;a href=&quot;https://blog.chromium.org/2020/05/introducing-web-vitals-essential-metrics.html&quot;&gt;May of
2020&lt;/a&gt;
and, since then, its role in Search has morphed and evolved as roll-outs have
been made and feedback has been received.&lt;/p&gt;

&lt;p&gt;However, to this day, messaging from Google can seem somewhat unclear and, in
places, even contradictory. In this post, I am going to distil everything that
you actually &lt;em&gt;need&lt;/em&gt; to know using fully referenced and cited Google sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t have time to read 5,500+ words?&lt;/strong&gt; Need to get this message across to
your entire company? &lt;a href=&quot;/contact/?utm_campaign=cwv-seo&quot;&gt;Hire me&lt;/a&gt; to deliver this
talk internally.&lt;/p&gt;

&lt;p&gt;If you’re happy just to trust me, then this is all you need to know right now:&lt;/p&gt;

&lt;div class=&quot;c-highlight  mb&quot;&gt;

&lt;p&gt;Google takes &lt;strong&gt;URL-level Core Web Vitals data from CrUX&lt;/strong&gt; into
account when deciding where to rank you in a search results page. They do not
use Lighthouse or PageSpeed Insights scores. That said, it is just one of many
different factors (or &lt;em&gt;signals&lt;/em&gt;) they use to determine your placement—the
best content still always wins.&lt;/p&gt;

&lt;p&gt;To get a ranking boost, you need to &lt;strong&gt;pass all relevant Core Web Vitals
&lt;em&gt;and&lt;/em&gt; everything else in the Page Experience report&lt;/strong&gt;. Google do
strongly encourage you to focus on site speed for better performance in Search,
but, if you don’t pass all relevant Core Web Vitals (and the applicable factors
from the Page Experience report) they will not push you down the rankings.&lt;/p&gt;

&lt;p&gt;All Core Web Vitals data used to rank you is taken from actual Chrome-based
traffic to your site. This means your &lt;strong&gt;rankings are reliant on your
performance in Chrome&lt;/strong&gt;, even if the majority of your customers are in
non-Chrome browsers. However, the search results pages themselves are
browser-agnostic: you’ll place the same for a search made in Chrome as you would
in Safari as you would in Firefox.&lt;/p&gt;

&lt;p&gt;Conversely, search results on desktop and mobile may appear different as
desktop searches will use desktop Core Web Vitals data and mobile searches will
use mobile data. This means that &lt;strong&gt;your placement on each device type is
based on your performance on each device type&lt;/strong&gt;. Interestingly, Google
have decided to keep the Core Web Vitals thresholds the same on both device
classifications. However, this is the full extent of the segmentation that they
make; slow experiences in, say, Australia, will negatively impact search results
in, say, the UK.&lt;/p&gt;

&lt;p&gt;If you’re a Single-Page Application (SPA), you’re out of luck. While Google
have made adjustments to not overly penalise you, &lt;strong&gt;your SPA is never
really going to make much of a positive impact where Core Web Vitals are
concerned&lt;/strong&gt;. In short, Google will treat a user’s landing page as the
source of its data, and any subsequent route change contributes nothing.
Therefore, optimise every SPA page for a first-time visit.&lt;/p&gt;

&lt;p&gt;The best place to find &lt;strong&gt;the data that Google holds on your site is
Search Console&lt;/strong&gt;. While sourced from CrUX, it’s here that is distilled
into actionable, Search-facing data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The true impact of Core Web Vitals on ranking is not fully
understood&lt;/strong&gt;, but investing in faster pages is still a sensible
endeavour for almost any reason you care to name.&lt;/p&gt;

&lt;/div&gt;

&lt;p&gt;Now would be a good time to mention: &lt;strong&gt;I am an independent web performance
consultant&lt;/strong&gt;—one of the best. I am available to help you find and fix your
site-speed issues through &lt;a href=&quot;/code-reviews/?utm_campaign=cwv-seo&quot;&gt;performance
audits&lt;/a&gt;, &lt;a href=&quot;/workshops/?utm_campaign=cwv-seo&quot;&gt;training and
workshops&lt;/a&gt;,
&lt;a href=&quot;/consultancy/?utm_campaign=cwv-seo&quot;&gt;consultancy&lt;/a&gt;, and more. You should &lt;a href=&quot;/contact/?utm_campaign=cwv-seo&quot;&gt;get in
touch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For citations, quotes, proof, and evidence, read on…&lt;/p&gt;

&lt;h2 id=&quot;site-speed-is-more-than-seo&quot;&gt;Site-Speed Is More Than SEO&lt;/h2&gt;

&lt;p&gt;While this article is an objective look at the role of Core Web Vitals in SEO,
I want to take one section to add my own thoughts to the mix. While Core Web
Vitals can help with SEO, there’s so much more to site-speed than that.&lt;/p&gt;

&lt;p&gt;Yes, SEO helps get people to your site, but their experience while they’re there
is a far bigger predictor of whether they are likely to convert or not.
Improving Core Web Vitals is likely to improve your rankings, but there are
myriad other reasons to focus on site-speed outside of SEO.&lt;/p&gt;

&lt;p&gt;I’m happy that Google’s Core Web Vitals initiative has put site-speed on the
radar of so many individuals and organisations, but I’m keen to stress that
optimising for SEO is only really the start of your web performance journey.&lt;/p&gt;

&lt;p&gt;With that said, everything from this point on is talking purely about optimising
Core Web Vitals for SEO, and does not take the user experience into account.
Ultimately, everything is all, always about the user experience, so improving
Core Web Vitals irrespective of SEO efforts should be assumed a good decision.&lt;/p&gt;

&lt;h3 id=&quot;the-core-web-vitals-metrics&quot;&gt;The Core Web Vitals Metrics&lt;/h3&gt;

&lt;p&gt;Generally, I approve of the Core Web Vitals metrics themselves (&lt;a href=&quot;https://web.dev/lcp/&quot;&gt;Largest
Contentful Paint&lt;/a&gt;, &lt;a href=&quot;https://web.dev/fid/&quot;&gt;First Input
Delay&lt;/a&gt;, &lt;a href=&quot;https://web.dev/cls/&quot;&gt;Cumulative Layout Shift&lt;/a&gt;,
and the nascent &lt;a href=&quot;https://web.dev/inp/&quot;&gt;Interaction to Next Paint&lt;/a&gt;). I think they
do a decent job of quantifying the user experience in a broadly applicable
manner and I’m happy that the Core Web Vitals team constantly evolve or even
replace the metrics in response to changes in the landscape.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/cwv-metrics.png&quot; alt=&quot;Graphic showing the three current Core Web Vitals and their thresholds&quot; width=&quot;1500&quot; height=&quot;351&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;— &lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Web Vitals&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I still feel that site owners who are serious about web performance should
augment Core Web Vitals with their own custom metrics (e.g. ‘largest content’ is
not the same as ‘most important content’), but as off-the-shelf metrics go, Core
Web Vitals are the best user-facing metrics since &lt;a href=&quot;https://twitter.com/patmeenan&quot;&gt;Patrick
Meenan&lt;/a&gt;’s work on
&lt;a href=&quot;https://developer.chrome.com/en/docs/lighthouse/performance/speed-index/&quot;&gt;SpeedIndex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;strong&gt;N.B.&lt;/strong&gt; In March 2024, First Input Delay (FID) will be
removed, and Interaction to Next Paint (INP) will take its place. –
&lt;a href=&quot;https://web.dev/inp-cwv/&quot;&gt;Advancing Interaction to Next
Paint&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;some-history&quot;&gt;Some History&lt;/h2&gt;

&lt;p&gt;Google has actually used Page Speed in rankings in some form or another since as
early as 2010:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;As part of that effort, today we’re including a new signal in our search
ranking algorithms: site speed.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking&quot;&gt;Using site speed in web search ranking&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in 2018, that was rolled out to mobile:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Although speed has been used in ranking for some time, that signal was focused
on desktop searches. Today we’re announcing that starting in July 2018, page
speed will be a ranking factor for mobile searches.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2018/01/using-page-speed-in-mobile-search&quot;&gt;Using page speed in mobile search ranking&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The criteria was undefined, and we were offered little more than &lt;q&gt;it applies
the same standard to all pages, regardless of the technology used to build the
page.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, even back then, Google made it clear that the best content would
always win, and that relevance was still the strongest signal. From 2010:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;While site speed is a new signal, it doesn’t carry as much weight as the
relevance of a page.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking&quot;&gt;Using site speed in web search ranking&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And again in 2018:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The intent of the search query is still a very strong signal, so a slow page
may still rank highly if it has great, relevant content.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2018/01/using-page-speed-in-mobile-search&quot;&gt;Using page speed in mobile search ranking&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In that case, let’s talk about relevance and content…&lt;/p&gt;

&lt;h2 id=&quot;the-best-content-always-wins&quot;&gt;The Best Content Always Wins&lt;/h2&gt;

&lt;p&gt;Google’s mission is to surface the best possible response to a user’s query,
which means they prioritise relevant content above all else. Even if a site is
slow, insecure, and not mobile friendly, it will rank first if it is exactly
what a user is looking for.&lt;/p&gt;

&lt;p&gt;In the event that there are a number of possible matches, Google will begin to
look at other ranking signals to further arrange the hierarchy of results. To
this end, Core Web Vitals (and all other ranking signals) should be thought of
as tie-breakers:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Google Search always seeks to show the most relevant content, even if the page
experience is sub-par. But for many queries, there is lots of helpful content
available. &lt;strong&gt;Having a great page experience can contribute to success in
Search&lt;/strong&gt;, in such cases.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The latter half of that paragraph is of particular interest to us, though: Core
Web Vitals do still matter…&lt;/p&gt;

&lt;article class=&quot;[ box  box--highlight ]  [ flag  flag--responsive ]  mb&quot; data-ui-component=&quot;Cross-sell promo&quot;&gt;
  &lt;div class=&quot;flag__img&quot;&gt;&lt;a href=&quot;/contact/?utm_campaign=cta-article-promo&quot; class=&quot;btn btn--full&quot;&gt;Get in touch&lt;/a&gt;&lt;/div&gt;
  &lt;div class=&quot;flag__body&quot;&gt;
    &lt;span class=&quot;heading  mb0&quot;&gt;Need Some Help?&lt;/span&gt;
    &lt;p&gt;I help companies find and fix site-speed issues. &lt;b&gt;Performance audits&lt;/b&gt;, &lt;b&gt;training&lt;/b&gt;, &lt;b&gt;consultancy&lt;/b&gt;, and more.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;

&lt;h2 id=&quot;core-web-vitals-are-important&quot;&gt;Core Web Vitals Are Important&lt;/h2&gt;

&lt;p&gt;Though it’s true we have to prioritise the best and most relevant content,
Google still stresses the importance of site speed if you care about rankings:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We highly recommend site owners &lt;strong&gt;achieve good Core Web Vitals&lt;/strong&gt; for success
with Search…&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/core-web-vitals&quot;&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That in itself is a strong indicator that Google favours faster websites.
Furthermore, they add:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Google’s core ranking systems look to &lt;strong&gt;reward content that provides a good
page experience&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which brings me nicely onto…&lt;/p&gt;

&lt;h2 id=&quot;its-not-just-about-core-web-vitals&quot;&gt;It’s Not Just About Core Web Vitals&lt;/h2&gt;

&lt;p&gt;What’s this phrase &lt;q&gt;page experience&lt;/q&gt; that we keep hearing about?&lt;/p&gt;

&lt;p&gt;It turns out that Core Web Vitals on their own are not enough. Core Web Vitals
are a subset of &lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;the Page Experience
report&lt;/a&gt;, and it’s
actually this that you need to pass in order to get a boost in rankings.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;https://developers.google.com/search/blog/2020/05/evaluating-page-experience&quot;&gt;May
2020&lt;/a&gt;,
Google announced the Page Experience report, and, a year later, from &lt;a href=&quot;https://developers.google.com/search/blog/2021/04/more-details-page-experience&quot;&gt;June to
August
2021&lt;/a&gt;,
they rolled it out for mobile. Also in &lt;a href=&quot;https://developers.google.com/search/blog/2021/08/simplifying-the-page-experience-report&quot;&gt;August
2021&lt;/a&gt;,
they removed Safe Browsing and Ad Experience from the report, and in &lt;a href=&quot;https://developers.google.com/search/blog/2021/11/bringing-page-experience-to-desktop&quot;&gt;February
2022&lt;/a&gt;,
they rolled Page Experience out for desktop.&lt;/p&gt;

&lt;p&gt;The simplified Page Experience report contains:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Core Web Vitals
    &lt;ul&gt;
      &lt;li&gt;Largest Contentful Paint&lt;/li&gt;
      &lt;li&gt;First Input Delay&lt;/li&gt;
      &lt;li&gt;Cumulative Layout Shift&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Mobile Friendly (mobile only, naturally)&lt;/li&gt;
  &lt;li&gt;HTTPS&lt;/li&gt;
  &lt;li&gt;No Intrusive Interstitials&lt;/li&gt;
&lt;/ul&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/page-experience.png&quot; alt=&quot;Graphic showing how the Page Experience report actually contains Core Web Vitals as a subset of requirements&quot; width=&quot;960&quot; height=&quot;540&quot; loading=&quot;lazy&quot; style=&quot;mix-blend-mode: multiply;&quot; /&gt;
  &lt;figcaption&gt;— &lt;a href=&quot;https://developers.google.com/search/blog/2021/08/simplifying-the-page-experience-report&quot;&gt;Simplifying the Page Experience report&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;From Google:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;…&lt;strong&gt;great page experience involves more than Core Web Vitals&lt;/strong&gt;. Good stats
within the Core Web Vitals report in Search Console or third-party Core Web
Vitals reports don’t guarantee good rankings.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What this means is we shouldn’t be focusing &lt;em&gt;only&lt;/em&gt; on Core Web Vitals, but on
the whole suite of Page Experience signals. That said, Core Web Vitals are quite
a lot more difficult to achieve than being mobile friendly, which is usually
baked in from the beginning of a project.&lt;/p&gt;

&lt;h2 id=&quot;you-dont-need-to-pass-fid&quot;&gt;You Don’t Need to Pass FID&lt;/h2&gt;

&lt;p&gt;You don’t &lt;em&gt;need&lt;/em&gt; to pass First Input Delay. This is because—while all pages will
have a Largest Contentful Paint event at some point, and the ideal Cumulative
Layout Shift score &lt;em&gt;is&lt;/em&gt; none at all—not all pages will incur a user interaction.
While rare, it is possible that a URL’s FID data will read &lt;em&gt;Not enough data&lt;/em&gt;.
To this end, passing Core Web Vitals means &lt;em&gt;Good&lt;/em&gt; LCP and CLS, and &lt;em&gt;Good&lt;/em&gt; or
&lt;em&gt;Not enough data&lt;/em&gt; FID.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The URL has Good status in the Core Web Vitals  in both CLS and LCP, &lt;strong&gt;and
Good (or not enough data) in FID&lt;/strong&gt;&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;Page Experience report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;interaction-to-next-paint-doesnt-matter-yet&quot;&gt;Interaction to Next Paint Doesn’t Matter Yet&lt;/h2&gt;

&lt;p&gt;Search Console, and other tools, are surfacing INP already, but it won’t become
a Core Web Vital (and therefore part of Page Experience (and therefore part of
the ranking signal)) until March 2024:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;INP (Interaction to Next Paint) is a new metric that will replace FID (First
Input Delay) as a Core Web Vital in March 2024. Until then, INP is not a part
of Core Web Vitals. &lt;strong&gt;Search Console reports INP data to help you prepare.&lt;/strong&gt;&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Incidentally, although INP isn’t yet a Core Web Vital, Search Console has
started sending emails warning site owners about INP issues:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/inp-email.png&quot; width=&quot;2086&quot; height=&quot;1870&quot; loading=&quot;lazy&quot; alt=&quot;Screenshot showing an example email that Search Console has begun sending site owners to warn them about INP issues&quot; style=&quot;mix-blend-mode: multiply;&quot; /&gt;
  &lt;figcaption&gt;Search Console emails have begun warning people about INP issues. Credit: &lt;a href=&quot;https://twitter.com/ryantownsend&quot;&gt;Ryan Townsend&lt;/a&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;You don’t need to worry about it yet, but do make sure it’s on your roadmap.&lt;/p&gt;

&lt;h2 id=&quot;youre-ranked-on-individual-urls&quot;&gt;You’re Ranked on Individual URLs&lt;/h2&gt;

&lt;p&gt;This has been one of the most persistently confusing aspect of Core Web Vitals:
are pages ranked on their individual URL status, or the status of the URL Group
they live in (or something else entirely)?&lt;/p&gt;

&lt;p&gt;It’s done on a per-URL basis:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/url-table.png&quot; width=&quot;1408&quot; height=&quot;439&quot; loading=&quot;lazy&quot; alt=&quot;Screenshot of a table showing that Core Web Vitals are judged at URL-level in Search&quot; /&gt;
  &lt;figcaption&gt;— &lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;&lt;cite&gt;Page Experience report&lt;/cite&gt;&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Google evaluates page experience metrics for individual URLs&lt;/strong&gt; on your site
and will use them as a ranking signal for a URL in Google Search results.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;Page Experience report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are also URL Groups and larger groupings of URL data:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Our core ranking systems generally evaluate content on a page-specific basis
[…] However, &lt;strong&gt;we do have some site-wide assessments&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If there isn’t enough data for a specific URL Group, Google will fall back to an
origin-level assessment:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If a URL group doesn’t have enough information to display in the report,
&lt;strong&gt;Search Console creates a higher-level origin group&lt;/strong&gt;…&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This doesn’t tell us &lt;em&gt;why&lt;/em&gt; we have URL Groups in the first place. How do they
tie into SEO and rankings if we work on a URL- or site-level basis?&lt;/p&gt;

&lt;p&gt;My feeling is that it’s less about rankings and more about helping developers
troubleshoot issues in bulk:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;URLs in the report are grouped [and] it is assumed that these groups have
a common framework and the reasons for any poor behavior of the group will
likely be caused by the same underlying reasons.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;URLs are judged on the three Core Web Vitals, which means they could be &lt;em&gt;Good&lt;/em&gt;,
&lt;em&gt;Needs Improvement&lt;/em&gt;, and &lt;em&gt;Poor&lt;/em&gt; in each Vital respectively. Unfortunately, URLs
are ranked on their lowest common denominator: if a URL is &lt;em&gt;Good&lt;/em&gt;, &lt;em&gt;Good&lt;/em&gt;,
&lt;em&gt;Poor&lt;/em&gt;, it’s marked &lt;em&gt;Poor&lt;/em&gt;. If it’s &lt;em&gt;Needs Improvement&lt;/em&gt;, &lt;em&gt;Good&lt;/em&gt;, &lt;em&gt;Needs
Improvement&lt;/em&gt;, it’s marked &lt;em&gt;Needs Improvement&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The status for a URL group defaults to the slowest status assigned to it for
that device type…&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The URLs that appear in Search Console are non-canonical. This means that
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://shop.com/products/red-bicycle&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://shop.com/bikes/red-bicycle&lt;/code&gt;
may both be listed in the report even if their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rel=canonical&lt;/code&gt; both point to the
same location.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Data is assigned to the actual URL, not the canonical URL, as it is in most
other reports.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note that this only discusses the report and not rankings—it is my understanding
that this is to help developers find variations of pages that are slower, and
not to rank multiple variants of the same URL. The latter would contravene their
own rules on canonicalisation:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Google can only index the canonical URL from a set of duplicate pages.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/10347851?hl=en&quot;&gt;Canonical&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or, expressed a little more logically, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;canonical&lt;/code&gt; alternative (and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;noindex&lt;/code&gt;)
pages can’t appear in Search in the first place, so there’s little point
worrying about Core Web Vitals for SEO in this case anyway.&lt;/p&gt;

&lt;article class=&quot;[ box  box--highlight ]  [ flag  flag--responsive ]  mb&quot; data-ui-component=&quot;Cross-sell promo&quot;&gt;
  &lt;div class=&quot;flag__img&quot;&gt;&lt;a href=&quot;/contact/?utm_campaign=cta-article-promo&quot; class=&quot;btn btn--full&quot;&gt;Get in touch&lt;/a&gt;&lt;/div&gt;
  &lt;div class=&quot;flag__body&quot;&gt;
    &lt;span class=&quot;heading  mb0&quot;&gt;Need Some Help?&lt;/span&gt;
    &lt;p&gt;I help companies find and fix site-speed issues. &lt;b&gt;Performance audits&lt;/b&gt;, &lt;b&gt;training&lt;/b&gt;, &lt;b&gt;consultancy&lt;/b&gt;, and more.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;

&lt;p&gt;Interestingly:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Core Web Vitals URLs include URL parameters when distinguishing the page;
PageSpeed Insights strips all parameter data from the URL, and then assigns
all results to the bare URL.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that if we were to drop &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://shop.com/products?sort=descending&lt;/code&gt;
into &lt;a href=&quot;https://pagespeed.web.dev&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pagespeed.web.dev&lt;/code&gt;&lt;/a&gt;, the Core Web Vitals it
presents back would be the data for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://shop.com/products&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;search-console-is-gospel&quot;&gt;Search Console Is Gospel&lt;/h2&gt;

&lt;p&gt;When looking into Core Web Vitals for SEO purposes, the only real place to
consult is Search Console. Core Web Vitals information is surfaced in a number
of different Google properties, and is underpinned by data sourced from the
Chrome User Experience Report, or CrUX:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;CrUX is the official dataset of the Web Vitals program.&lt;/strong&gt; All user-centric
Core Web Vitals metrics will be represented in the dataset.&lt;br /&gt;
— &lt;a href=&quot;https://developer.chrome.com/docs/crux/about/&quot;&gt;About CrUX&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The data for the Core Web Vitals report comes from the CrUX report.&lt;/strong&gt; The
CrUX report gathers anonymized metrics about performance times from actual
users visiting your URL (called field data). The CrUX database gathers
information about URLs whether or not the URL is part of a Search Console
property.&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the data that is then used in Search to influence rankings:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The data collected by CrUX is available publicly through a number of tools and
&lt;strong&gt;is used by Google Search to inform the page experience ranking factor&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://developer.chrome.com/docs/crux/about/&quot;&gt;About CrUX&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The data is then surfaced to us in Search Console.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Search Console shows how CrUX data influences the page experience ranking
factor&lt;/strong&gt; by URL and URL group.&lt;br /&gt;
— &lt;a href=&quot;https://developer.chrome.com/docs/crux/methodology/#tool-gsc&quot;&gt;CrUX methodology&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Basically, the data originates in CrUX, so it’s CrUX all the way down, but it’s
in Search Console that Google kindly aggregates, segments, and otherwise
visualises and displays the data to make it actionable. Google expects you to
look to Search Console to find and fix your Core Web Vitals issues:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Google Search Console provides a dedicated report to help site owners quickly
identify opportunities for improvement.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2020/05/evaluating-page-experience&quot;&gt;Evaluating page experience for a better web&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;ignore-lighthouse-and-pagespeed-insights-scores&quot;&gt;Ignore Lighthouse and PageSpeed Insights Scores&lt;/h2&gt;

&lt;p&gt;This is one of the most pervasive and definitely the most common
misunderstandings I see surrounding site-speed and SEO. Your Lighthouse
Performance scores have absolutely no bearing on your rankings. None whatsoever.
As before, the data Google use to influence rankings is stored in Search
Console, and you won’t find a single Lighthouse score in there.&lt;/p&gt;

&lt;p&gt;Frustratingly, there is no black-and-white statement from Google that tells us
&lt;q&gt;we do not use Lighthouse scores in ranking&lt;/q&gt;, but we can prove the
equivalent quite quickly:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The Core Web Vitals report shows how your pages perform, &lt;strong&gt;based on real world
usage data (sometimes called field data)&lt;/strong&gt;.&lt;br /&gt;
– &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The data for the Core Web Vitals report comes from the CrUX report. The CrUX
report gathers anonymized metrics about performance times &lt;strong&gt;from actual users
visiting your URL (called field data)&lt;/strong&gt;.&lt;br /&gt;
– &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s two definitive statements saying where the data &lt;em&gt;does&lt;/em&gt; come from: the
field. So any data that doesn’t come from the field is not counted.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;PSI provides both lab and field data about a page.&lt;/strong&gt; Lab data is useful for
debugging issues, as it is collected in a controlled environment. However, it
may not capture real-world bottlenecks. Field data is useful for capturing
true, real-world user experience – but has a more limited set of metrics.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/speed/docs/insights/v5/about&quot;&gt;About PageSpeed Insights&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the past—and I can’t determine the exact date of the following
screenshot—Google used to clearly mark &lt;i&gt;lab&lt;/i&gt; and &lt;i&gt;field&lt;/i&gt; data in
PageSpeed Insights:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/psi-legacy.png&quot; width=&quot;1500&quot; height=&quot;1247&quot; loading=&quot;lazy&quot; alt=&quot;Screenshot showing PageSpeed Insights clearly labelling lab and field in the past.&quot; /&gt;
  &lt;figcaption&gt;— &lt;a href=&quot;https://www.sistrix.com/ask-sistrix/onpage-optimisation/google-pagespeed-the-loading-speed-of-a-website/what-is-google-pagespeed-insights&quot;&gt;&lt;cite&gt;What is Google PageSpeed Insights?&lt;/cite&gt;&lt;/a&gt; – SISTRIX&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Nowadays, the same data and layout exists, but with much less deliberate
wording. Field data is still presented first:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/psi-field.png&quot; alt=&quot;A recent PageSpeed Insights screenshot showing less clear wording around field data&quot; width=&quot;1500&quot; height=&quot;787&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Here we can see that this data came from CrUX and is based on real, aggregated data.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And lab data, from the Lighthouse test we just initiated, beneath that:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/psi-lab.png&quot; alt=&quot;A recent PageSpeed Insights screenshot showing less clear wording around lab data&quot; width=&quot;1500&quot; height=&quot;1329&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Here we can clearly see that this was run from a predetermined location, on a predetermined device, over a predetermined connection speed. This was one page load run by us, for us.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;So for all there is no definitive warning from Google that we shouldn’t factor
Lighthouse Performance scores into SEO, we can quickly piece together the
information ourselves. It’s more a case of what they haven’t said, and nowhere
have they ever said your Lighthouse/PageSpeed scores impact rankings.&lt;/p&gt;

&lt;p&gt;On the subject of things they haven’t said…&lt;/p&gt;

&lt;h2 id=&quot;failing-pages-dont-get-penalised&quot;&gt;Failing Pages Don’t Get Penalised&lt;/h2&gt;

&lt;p&gt;This is a critical piece of information that is almost impressively-well hidden.&lt;/p&gt;

&lt;p&gt;Google tell us that the criteria for a &lt;em&gt;Good&lt;/em&gt; page experience are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Passes all relevant Core Web Vitals&lt;/li&gt;
  &lt;li&gt;No mobile usability issues on mobile&lt;/li&gt;
  &lt;li&gt;Served over HTTPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a URL achieves &lt;em&gt;Good&lt;/em&gt; status, that status will be used as a ranking signal in
search results.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/pass-fail.png&quot; alt=&quot;Screenshot showing that URLs will be marked up in Search if they pass all Page Experience signals, but not showing that they would get marked down for not passing&quot; widthg=&quot;1367&quot; height=&quot;721&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;— &lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;Page Experience report&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Note the absence of similar text under the &lt;em&gt;Failed&lt;/em&gt; column. &lt;em&gt;Good&lt;/em&gt; URLs’ status
will be used as a ranking signal, Failed URLs… nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Good&lt;/em&gt; URLs’ status will be used as a ranking signal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All of Google’s wording around Core Web Vitals is about rewarding &lt;em&gt;Good&lt;/em&gt;
experiences, and never about suppressing &lt;em&gt;Poor&lt;/em&gt; ones:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We highly recommend site owners achieve &lt;strong&gt;good Core Web Vitals for success
with Search&lt;/strong&gt;…&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/core-web-vitals&quot;&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;Google’s core ranking systems look to &lt;strong&gt;reward content that provides a good
page experience&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;…for many queries, there is lots of helpful content available. Having
a &lt;strong&gt;great page experience can contribute to success&lt;/strong&gt; in Search….&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;small&gt;Note that this is in contrast to their &lt;a href=&quot;https://developers.google.com/search/blog/2018/01/using-page-speed-in-mobile-search&quot;&gt;2018
announcement&lt;/a&gt; which stated that &lt;q&gt;The “Speed Update” […] will only affect
pages that deliver the slowest experience to users…&lt;/q&gt; – &lt;i&gt;Speed Update&lt;/i&gt;
was a precursor to Core Web Vitals.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;This means that failing URLs will not get pushed down the search results page,
which is probably a huge and overdue relief for many of you reading this.
However…&lt;/p&gt;

&lt;p&gt;If one of your competitors puts in a huge effort to improve their Page
Experience and begins moving up the search results pages, that will have the net
effect of pushing you down.&lt;/p&gt;

&lt;p&gt;Put another way, while you won’t be penalised, you might not get to simply stay
where you are. Which means…&lt;/p&gt;

&lt;h2 id=&quot;core-web-vitals-are-a-tie-breaker&quot;&gt;Core Web Vitals Are a Tie-Breaker&lt;/h2&gt;

&lt;p&gt;Core Web Vitals really shine in competitive environments, or when users aren’t
searching for something that only you could possibly provide. When Google could
rank a number of different URLs highly, it defers to other ranking signals to
refine its ordering.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;…for many queries, there is lots of helpful content available. &lt;strong&gt;Having
a great page experience can contribute to success in Search&lt;/strong&gt;, in such cases.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;article class=&quot;[ box  box--highlight ]  [ flag  flag--responsive ]  mb&quot; data-ui-component=&quot;Cross-sell promo&quot;&gt;
  &lt;div class=&quot;flag__img&quot;&gt;&lt;a href=&quot;/contact/?utm_campaign=cta-article-promo&quot; class=&quot;btn btn--full&quot;&gt;Get in touch&lt;/a&gt;&lt;/div&gt;
  &lt;div class=&quot;flag__body&quot;&gt;
    &lt;span class=&quot;heading  mb0&quot;&gt;Need Some Help?&lt;/span&gt;
    &lt;p&gt;I help companies find and fix site-speed issues. &lt;b&gt;Performance audits&lt;/b&gt;, &lt;b&gt;training&lt;/b&gt;, &lt;b&gt;consultancy&lt;/b&gt;, and more.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;

&lt;h2 id=&quot;there-are-no-shades-of-good-or-failed-urls&quot;&gt;There Are No Shades of Good or Failed URLs&lt;/h2&gt;

&lt;p&gt;Going back to the &lt;em&gt;Good&lt;/em&gt; versus &lt;em&gt;Failed&lt;/em&gt; columns above, notice that it’s
binary—there are no grades of &lt;em&gt;Good&lt;/em&gt; or &lt;em&gt;Failed&lt;/em&gt;—it’s just one or the other.
A URL is considered &lt;em&gt;Failed&lt;/em&gt; the moment it doesn’t pass even one of the relevant
Core Web Vitals, which means a Largest Contentful Paint of 2.6s is just as bad
as a Largest Contentful Paint of 26s.&lt;/p&gt;

&lt;p&gt;Put another way, anything other than &lt;em&gt;Good&lt;/em&gt; is &lt;em&gt;Failed&lt;/em&gt;, so the actual numbers
are irrelevant.&lt;/p&gt;

&lt;h2 id=&quot;mobile-and-desktop-thresholds-are-the-same&quot;&gt;Mobile and Desktop Thresholds Are the Same&lt;/h2&gt;

&lt;p&gt;Interestingly, the thresholds for &lt;em&gt;Good&lt;/em&gt;, &lt;em&gt;Needs Improvement&lt;/em&gt;, and &lt;em&gt;Poor&lt;/em&gt; are
the same on both mobile and desktop. Because Google announced Core Web Vitals
for mobile first, the same thresholds on desktop should be achieved
automatically—it’s very rare that desktop experiences would fare worse than
mobile ones. The only exception might be Cumulative Layout Shift in which
desktop devices have more screen real estate for things to move around.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;For each of the above metrics, to ensure you’re hitting the recommended target
for most of your users, a good threshold to measure is the 75th percentile of
page loads, segmented &lt;strong&gt;across mobile and desktop devices&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Web Vitals&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This does help simplify things a little, with only one set of numbers to
remember.&lt;/p&gt;

&lt;h2 id=&quot;slow-countries-can-harm-global-rankings&quot;&gt;Slow Countries Can Harm Global Rankings&lt;/h2&gt;

&lt;p&gt;While Google does segment on desktop and mobile—ranking you on each device type
proportionate to your performance on each device type—that’s as far at they go.
This means that if an experience is &lt;em&gt;Poor&lt;/em&gt; on mobile but &lt;em&gt;Good&lt;/em&gt; on desktop,
any searches for you on desktop will have your fast site taken into
consideration.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/cwv-map.png&quot; alt=&quot;A screenshot of Treo showing Core Web Vitals data from CrUX segmented by country, displayed on a world map&quot; widthg=&quot;1500&quot; height=&quot;884&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;https://treo.sh/&quot;&gt;Treo&lt;/a&gt; makes it easy to visualise global CrUX data.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Unfortunately, that’s as far as their segmentation goes, and even though CrUX
does capture country-level data:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;…we are expanding the existing CrUX dataset […] to also include a collection
of separate country-specific datasets!&lt;br /&gt;
— &lt;a href=&quot;https://developer.chrome.com/blog/crux-2018-01/&quot;&gt;Chrome User Experience Report - New country dimension&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…it does not make its way into Search Console or
any ranking decision:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Remember that data is combined for all requests from all locations. &lt;strong&gt;If you
have a substantial amount of traffic from a country with, say, slow internet
connections, then your performance in general will go down.&lt;/strong&gt;&lt;br /&gt;
— &lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately, for now at least, this means that if the majority of your paying
customers are in a region that enjoys &lt;em&gt;Good&lt;/em&gt; experiences, but you have a lot of
traffic from regions that suffer &lt;em&gt;Poor&lt;/em&gt; experiences, those worse data points may
be negatively impacting your success elsewhere.&lt;/p&gt;

&lt;h2 id=&quot;ios-and-other-traffic-doesnt-count&quot;&gt;iOS (and Other) Traffic Doesn’t Count&lt;/h2&gt;

&lt;p&gt;Core Web Vitals is a Chrome initiative—evidenced by &lt;em&gt;Chrome&lt;/em&gt; User Experience
Report, among other things. The APIs used to capture the three Core Web Vitals
are available in &lt;a href=&quot;https://en.wikipedia.org/wiki/Blink_(browser_engine)&quot;&gt;Blink&lt;/a&gt;,
the browser engine that powers Chromium-based browsers such as Chrome, Edge, and
Opera. While the APIs are available to these non-Chrome browsers, only Chrome
currently captures data themselves, and populates the &lt;em&gt;Chrome&lt;/em&gt; User Experience
Report from there. So, Blink-based browsers have the Core Web Vitals APIs, but
only Chrome captures data for CrUX.&lt;/p&gt;

&lt;p&gt;It should be, hopefully, fairly obvious that non-Chrome browsers such as Firefox
or Edge would not contribute data to the &lt;em&gt;Chrome&lt;/em&gt; User Experience Report, but
what about Chrome on iOS? That is called Chrome, after all?&lt;/p&gt;

&lt;p&gt;Unfortunately, while Chrome on iOS is a project owned by the Chromium team, the
browser itself does not use Blink—the only engine that can currently capture
Core Web Vitals data:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Due to constraints of the iOS platform, all browsers must be built on top of
the WebKit rendering engine.&lt;/strong&gt; For Chromium, this means supporting both WebKit
as well as Blink, Chrome’s rendering engine for other platforms.&lt;br /&gt;
— &lt;a href=&quot;https://blog.chromium.org/2017/01/open-sourcing-chrome-on-ios.html&quot;&gt;Open-sourcing Chrome on iOS!&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From Apple themselves:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;2.5.6 Apps that browse the web &lt;strong&gt;must use the appropriate WebKit framework&lt;/strong&gt;
and WebKit JavaScript.&lt;br /&gt;
— &lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/&quot;&gt;App Store Review Guidelines&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Any browser on the iOS platform—Chrome, Firefox, Edge, Safari, you name it—uses
WebKit, and the APIs that power Core Web Vitals aren’t currently available
there:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint#browser_compatibility&quot;&gt;LargestContentfulPaint&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming#browser_compatibility&quot;&gt;PerformanceEventTiming&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift#browser_compatibility&quot;&gt;LayoutShift&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From Google themselves:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;There are a few notable exceptions that do not provide data to the CrUX
dataset […] &lt;strong&gt;Chrome on iOS.&lt;/strong&gt;&lt;br /&gt;
— &lt;a href=&quot;https://developer.chrome.com/docs/crux/methodology/&quot;&gt;CrUX methodology&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key takeaway here is that Chrome on iOS is actually WebKit under the hood,
so capturing Core Web Vitals is not possible at all, for developers or for the
Chrome team.&lt;/p&gt;

&lt;h2 id=&quot;core-web-vitals-and-single-page-applications&quot;&gt;Core Web Vitals and Single Page Applications&lt;/h2&gt;

&lt;p&gt;If you’re building a Single-Page Application (SPA), you’re going to have to take
a different approach. Core Web Vitals was not designed with SPAs in mind, and
while Google have made efforts to mitigate undue penalties for SPAs, they don’t
currently provide any way for SPAs to shine.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;However, properly optimized &lt;strong&gt;MPAs do have some advantages in meeting the Core
Web Vitals thresholds that SPAs currently do not&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://web.dev/vitals-spa-faq/#is-it-harder-for-spas-to-do-well-on-core-web-vitals-than-mpas&quot;&gt;How SPA architectures affect Core Web Vitals&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Core Web Vitals data is captured for every page load, or &lt;em&gt;navigation&lt;/em&gt;. Because
SPAs don’t have traditional page loads, and instead have route changes, or &lt;em&gt;soft
navigations&lt;/em&gt;, they don’t emit a standardised way to tell Google that a page has
indeed changed. Because of this, Google has no way of capturing reliable Core
Web Vitals data for these non-standard soft navigations on which SPAs are built.&lt;/p&gt;

&lt;h3 id=&quot;the-first-page-view-is-all-that-counts&quot;&gt;The First Page View Is All That Counts&lt;/h3&gt;

&lt;p&gt;This is critical for optimising SPA Core Web Vitals for SEO purposes. Chrome
only captures data from the first page a user actually lands on:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Each of the Core Web Vitals metrics is measured relative to the current,
top-level page navigation. If a page dynamically loads new content and updates
the URL of the page in the address bar, it will have no effect on how the Core
Web Vitals metrics are measured. Metric values are not reset, and &lt;strong&gt;the URL
associated with each metric measurement is the URL the user navigated to that
initiated the page load&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://web.dev/vitals-spa-faq/#do-core-web-vitals-metrics-include-spa-route-transitions&quot;&gt;How SPA architectures affect Core Web Vitals&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Subsequent soft navigations are not registered, so you need to optimise every
page for a first-time visit.&lt;/p&gt;

&lt;p&gt;What is particularly painful here is that SPAs are notoriously bad at first-time
visits due to front-loading the entire application. They front-load this
application in order to make subsequent page views much faster, which is the one
thing Core Web Vitals will not measure. It’s a lose–lose. Sorry.&lt;/p&gt;

&lt;h3 id=&quot;the-near-future-doesnt-look-bright&quot;&gt;The (Near) Future Doesn’t Look Bright&lt;/h3&gt;

&lt;p&gt;Although Google are experimenting with defining soft navigations, any update or
change will not be seen in the CrUX dataset anytime soon:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The Chrome User Experience Report (CrUX) will ignore these additional values…
— &lt;a href=&quot;https://developer.chrome.com/blog/soft-navigations-experiment/&quot;&gt;Experimenting with measuring soft
navigations&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;chrome-have-done-things-to-help-mitigate&quot;&gt;Chrome Have Done Things to Help Mitigate&lt;/h3&gt;

&lt;p&gt;As soft navigations are not counted, the user’s landing page appears very long
lived: as far as Core Web Vitals sees, the user hasn’t ever left the first page
they came to. This means Core Web Vitals scores could grow dramatically out of
hand, counting &lt;var&gt;n&lt;/var&gt; page views against one unfortunate URL. To help
mitigate these blind spots inherent in not-using native web platform features,
Chrome have done a couple of things to not overly penalise SPAs.&lt;/p&gt;

&lt;p&gt;Firstly, Largest Contentful Paint stops being tracked after user interaction:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The browser will stop reporting new entries as soon as the user interacts with
the page.&lt;br /&gt;
— &lt;a href=&quot;https://web.dev/lcp/&quot;&gt;Largest Contentful Paint (LCP)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that the browser won’t keep looking for new LCP candidates as the
user traverses soft navigations—it would be very detrimental if a new route
loading at 120 seconds fired a new LCP event against the initial URL.&lt;/p&gt;

&lt;p&gt;Similarly, Cumulative Layout Shift was modified to be more sympathetic to
long-lived pages (e.g. SPAs):&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We (the Chrome Speed Metrics Team) recently outlined our initial research into
options for &lt;strong&gt;making the CLS metric more fair to pages that are open for
a long time&lt;/strong&gt;.&lt;br /&gt;
— &lt;a href=&quot;https://web.dev/evolving-cls/&quot;&gt;Evolving the CLS metric&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CLS takes the cumulative shifts in the most extreme five-second window, which
means that although CLS will constantly update throughout the whole SPA
lifecycle, only the worst five-second slice counts against you.&lt;/p&gt;

&lt;h3 id=&quot;these-mitigations-dont-help-us-much&quot;&gt;These Mitigations Don’t Help Us Much&lt;/h3&gt;

&lt;p&gt;No such mitigations have been made with First Input Delay or Interaction to Next
Paint, and none of these mitigations change the fact that you are effectively
only measured on the first page in a session, or that all subsequent updates to
a metric may count against the first URL a visitor encountered.&lt;/p&gt;

&lt;p&gt;Solutions are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Move to an MPA.&lt;/strong&gt; It’s probably going to be faster for most use cases
anyway.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optimise heavily for first visits.&lt;/strong&gt; This is Core Web Vitals-friendly, but
you’ll still only capture one URL’s worth of data per session.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cross your fingers and wait.&lt;/strong&gt; Work on new APIs is promising, and we can
only hope that this eventually gets incorporated into CrUX.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;we-dont-know-how-much-core-web-vitals-help&quot;&gt;We Don’t Know How Much Core Web Vitals Help&lt;/h2&gt;

&lt;p&gt;Historically, Google have never typically told us what weighting they give to
each of their ranking signals. The most insight we got was back in their 2010
announcement:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;While site speed is a new signal, it doesn’t carry as much weight as the
relevance of a page. &lt;strong&gt;Currently, fewer than 1% of search queries are affected
by the site speed signal&lt;/strong&gt; in our implementation and the signal for site speed
only applies for visitors searching in English on Google.com at this point. We
launched this change a few weeks back after rigorous testing. If you haven’t
seen much change to your site rankings, then this site speed change possibly
did not impact your site.&lt;br /&gt;
— &lt;a href=&quot;https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking&quot;&gt;Using site speed in web search ranking&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, this is completely separate to the Core Web Vitals initiative, where we
still have zero insight as to how much impact site-speed will have on rankings.&lt;/p&gt;

&lt;h2 id=&quot;measuring-the-impact-of-core-web-vitals-on-seo&quot;&gt;Measuring the Impact of Core Web Vitals on SEO&lt;/h2&gt;

&lt;p&gt;If Google won’t tell us, can we work it out ourselves?&lt;/p&gt;

&lt;p&gt;To the best of my knowledge, no one has done any meaningful study about just how
much &lt;em&gt;Good&lt;/em&gt; Page Experience might help organic rankings. The only way to really
work it out would be take some very solid baseline measurements of a set of
failing URLs, move them all into &lt;em&gt;Good&lt;/em&gt;, and then measure the uptick in organic
traffic to those pages. We’d also need to be very careful not to make any other
SEO-facing changes to those URLs for the duration of the experiment.&lt;/p&gt;

&lt;p&gt;Anecdotally, I do have one client that sees more than double average
click-through rate—and almost the same improvement in average position—for
&lt;em&gt;Good&lt;/em&gt; Page Experience over the site’s average. For them, the data suggests that
&lt;em&gt;Good&lt;/em&gt; Page Experience is highly impactful.&lt;/p&gt;

&lt;h2 id=&quot;so-what-do-we-do&quot;&gt;So, What Do We Do?!&lt;/h2&gt;

&lt;p&gt;Search is complicated and, understandably, quite opaque. Core Web Vitals and SEO
is, as we’ve seen, very intricate. But, my official advice, at a very high
level is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep focusing on producing high-quality, relevant content and work on
site-speed because it’s the right thing to do—everything else will follow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Faster websites benefit everyone: they convert better, they retain better,
they’re cheaper to run, they’re better for the environment, and they rank
better. &lt;strong&gt;There is no reason not to do it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you’d like help getting your Core Web Vitals in order, you can &lt;a href=&quot;/services/?utm_campaign=cwv-seo&quot;&gt;hire
me&lt;/a&gt;.&lt;/p&gt;

&lt;article class=&quot;[ box  box--highlight ]  [ flag  flag--responsive ]  mb&quot; data-ui-component=&quot;Cross-sell promo&quot;&gt;
  &lt;div class=&quot;flag__img&quot;&gt;&lt;a href=&quot;/contact/?utm_campaign=cta-article-promo&quot; class=&quot;btn btn--full&quot;&gt;Get in touch&lt;/a&gt;&lt;/div&gt;
  &lt;div class=&quot;flag__body&quot;&gt;
    &lt;span class=&quot;heading  mb0&quot;&gt;Need Some Help?&lt;/span&gt;
    &lt;p&gt;I help companies find and fix site-speed issues. &lt;b&gt;Performance audits&lt;/b&gt;, &lt;b&gt;training&lt;/b&gt;, &lt;b&gt;consultancy&lt;/b&gt;, and more.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;

&lt;h2 id=&quot;sources&quot;&gt;Sources&lt;/h2&gt;

&lt;p&gt;For this post, I have only taken official Google publications into account.
I haven’t included any information from Google employees’ Tweets, personal
sites, conference talks, etc. This is because there is no expectation or
requirement for non-official sources to edit or update their content as Core Web
Vitals information changes.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking&quot;&gt;Using site speed in web search ranking&lt;/a&gt; – Google Search Central Blog – 9 April 2010&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.chromium.org/2017/01/open-sourcing-chrome-on-ios.html&quot;&gt;Open-sourcing Chrome on iOS!&lt;/a&gt; – Chromium Blog – 31 January, 2017&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2018/01/using-page-speed-in-mobile-search&quot;&gt;Using page speed in mobile search ranking&lt;/a&gt; – Google Search Central Blog – 17 January 2018&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/blog/crux-2018-01/&quot;&gt;Chrome User Experience Report - New country dimension&lt;/a&gt; – Chrome Developers – 24 January, 2018&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://web.dev/lcp/&quot;&gt;Largest Contentful Paint (LCP)&lt;/a&gt; – web.dev – 8 August, 2019&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.chromium.org/2020/05/introducing-web-vitals-essential-metrics.html&quot;&gt;Introducing Web Vitals: essential metrics for a healthy site&lt;/a&gt; – Chromium Blog – 5 May, 2020&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2020/05/evaluating-page-experience&quot;&gt;Evaluating page experience for a better web&lt;/a&gt; – Google Search Central Blog – 28 May, 2020&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.google.com/webmasters/answer/10218333?hl=en&quot;&gt;Page Experience report&lt;/a&gt; – Search Console Help&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.google.com/webmasters/answer/9205520?hl=en&quot;&gt;Core Web Vitals report&lt;/a&gt; – Search Console Help&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.google.com/webmasters/answer/10347851?hl=en&quot;&gt;Canonical&lt;/a&gt; – Search Console Help&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt; – Google Search Central&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/appearance/core-web-vitals&quot;&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt; – Google Search Central&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2020/11/timing-for-page-experience&quot;&gt;Timing for bringing page experience to Google Search&lt;/a&gt; – Google Search Central Blog – 10 November, 2020&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://web.dev/evolving-cls/&quot;&gt;Evolving the CLS metric&lt;/a&gt; – web.dev – 7 April, 2021&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2021/04/more-details-page-experience?hl=en&quot;&gt;More time, tools, and details on the page experience update&lt;/a&gt; – Google Search Central Blog – 19 April, 2021&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2021/08/simplifying-the-page-experience-report&quot;&gt;Simplifying the Page Experience report&lt;/a&gt; – Google Search Central Blog – 4 August, 2021&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://web.dev/vitals-spa-faq/&quot;&gt;How SPA architectures affect Core Web Vitals&lt;/a&gt; – web.dev – 14 September 2021&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2021/11/bringing-page-experience-to-desktop&quot;&gt;Timeline for bringing page experience ranking to desktop&lt;/a&gt; – Google Search Central Blog – 4 November, 2021&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/crux/about/&quot;&gt;About CrUX&lt;/a&gt; – Chrome Developers 23 June, 2022&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/crux/methodology/&quot;&gt;CrUX methodology&lt;/a&gt; – Chrome Developers – 23 June, 2022&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/blog/soft-navigations-experiment/&quot;&gt;Experimenting with measuring soft navigations&lt;/a&gt; – Chrome Developers – 1 February, 2023&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2023/04/page-experience-in-search&quot;&gt;The role of page experience in creating helpful content&lt;/a&gt; – Google Search Central Blog – 19 April, 2023&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://web.dev/inp-cwv/&quot;&gt;Advancing Interaction to Next Paint&lt;/a&gt; – web.dev – 10 May, 2023&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/speed/docs/insights/v5/about&quot;&gt;About PageSpeed Insights&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/&quot;&gt;App Store Review Guidelines&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint#browser_compatibility&quot;&gt;LargestContentfulPaint&lt;/a&gt; – MDN&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming#browser_compatibility&quot;&gt;PerformanceEventTiming&lt;/a&gt; – MDN&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift#browser_compatibility&quot;&gt;LayoutShift&lt;/a&gt; – MDN&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 24 Jul 2023 00:00:00 +0000</pubDate>
        <link>https://csswizardry.com/2023/07/core-web-vitals-for-search-engine-optimisation/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/07/core-web-vitals-for-search-engine-optimisation/</guid>
      </item>
    
      <item>
        <title>The HTTP/1-liness of HTTP/2</title>
        <description>&lt;p class=&quot;c-highlight&quot;&gt;This article started life as &lt;a href=&quot;https://twitter.com/csswizardry/status/1678793192756355073&quot;&gt;a Twitter
thread&lt;/a&gt;, but I felt it needed a more permanent spot. You should &lt;a href=&quot;https://twitter.com/csswizardry&quot;&gt;follow me on Twitter&lt;/a&gt; if you don’t
already.&lt;/p&gt;

&lt;p&gt;I’ve been asked a few times—mostly in &lt;a href=&quot;/workshops/&quot;&gt;workshops&lt;/a&gt;—why HTTP/2 (H/2)
waterfalls often still look like HTTP/1.x (H/1). Why are things are done in
sequence rather than in parallel?&lt;/p&gt;

&lt;p&gt;Let’s unpack it!&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fair warning, I am going to oversimplify some terms and concepts. My goal
is to illustrate a point rather than explain the protocol in detail.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;One of the promises of H/2 was infinite parallel requests (up from the
historical six concurrent connections in H/1). So why does this H/2-enabled site
have such a staggered waterfall? This doesn’t look like H/2 at all!&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/waterfall-h2.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;2171&quot; /&gt;
&lt;figcaption&gt;This doesn’t look very parallelised!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Things get a little clearer if we add Chrome’s queueing time to the graph. All
of these files were discovered at the same time, but their requests were
dispatched in sequence.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/waterfall-h2-waiting.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;2171&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;The white bars show how long the browser queued the request for. All
files were discovered around 3.25s, but were all requested sometime after that.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;As a performance engineer, one of the first shifts in thought is that we don’t
care only about when resources were discovered or requests were dispatched (the
leftmost part of each entry). We also care about when responses are finished
(the rightmost part of each entry).&lt;/p&gt;

&lt;p&gt;When we stop and think about it, ‘when was a file useful?’ is much more
important than ‘when was a file discovered?’. Of course, a late-discovered file
will also be late-useful, but &lt;em&gt;really&lt;/em&gt; the only thing that matters is
usefulness.&lt;/p&gt;

&lt;p&gt;With H/2, yes, we can make far more requests at a time, but making more requests
doesn’t magically make everything faster. We’re still limited by device and
network constraints. We still have finite bandwidth, only now it needs sharing
among more files—it just gets diluted.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/playing-cards.png&quot; alt=&quot;&quot; width=&quot;160&quot; height=&quot;157&quot; style=&quot;float: left; margin-right: 1.5rem; margin-left: -1.5rem; shape-outside: url('https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/playing-cards.png');&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Let’s leave the web and HTTP for a second. Let’s play cards! Taylor, Charlie,
Sam, and Alex want to play cards. I am going to deal the cards to the four of
them.&lt;/p&gt;

&lt;p&gt;These four people and their cards represent downloading four files. Instead of
bandwidth, the constant here is that it takes me ONE SECOND to deal one card. No
matter how I do it, it will take me 52 seconds to finish the job.&lt;/p&gt;

&lt;p&gt;The traditional round-robin approach to dealing cards would be one to Taylor,
one to Charlie, one to Sam, one to Alex, and again and again until they’re all
dealt. Fifty-two seconds.&lt;/p&gt;

&lt;p&gt;This is what that looks like. It took 49 seconds before the first person had all
of their cards.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/cards-round-robin.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;2171&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Everything isn’t faster—everything is slower.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Can you see where this is going?&lt;/p&gt;

&lt;p&gt;What if I dealt each person all of their cards at once instead? Even with the
same overall 52-second timings, folk have a full hand of cards much sooner.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/cards-at-once.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;2171&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Half a JavaScript file is useless to us, so let’s focus on getting
complete responses over the wire as soon as possible.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Thankfully, the (s)lowest common denominator works just fine for a game of
cards. You can’t start playing before everyone has all of their cards anyway, so
there’s no need to ‘be useful’ much earlier than your friends.&lt;/p&gt;

&lt;p&gt;On the web, however, things are different. We don’t want files waiting on the
(s)lowest common denominator! We want files to arrive and be useful as soon as
possible. We don’t want a file at 49, 50, 51, 52s when we could have 13, 26, 39,
52!&lt;/p&gt;

&lt;p&gt;On the web, it turns out that some slightly H/1-like behaviour is still a good
idea.&lt;/p&gt;

&lt;p&gt;Back to our chart. Each of those files is &lt;a href=&quot;/2023/07/in-defence-of-domcontentloaded/&quot;&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JS
bundle&lt;/a&gt;, meaning they need to run in
sequence. Because of how everything is scheduled, requested, and prioritised, we
have an elegant pattern whereby files are queued, fetched, and executed in
a near-perfect order!&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/07/waterfall-h2-waiting.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;2171&quot; loading=&quot;lazy&quot; /&gt;
&lt;figcaption&gt;Hopefully it all makes a little more sense now.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Queue, fetch, execute, queue, fetch, execute, queue, fetch, execute, queue,
fetch, execute, queue, fetch, execute with almost zero dead time. This is the
height of elegance, and I love it.&lt;/p&gt;

&lt;p&gt;I fondly refer to this whole process as ‘orchestration’ because, truly, this is
artful to me. And that’s why your waterfalls look like that.&lt;/p&gt;
</description>
        <pubDate>Tue, 11 Jul 2023 20:30:54 +0000</pubDate>
        <link>https://csswizardry.com/2023/07/the-http1liness-of-http2/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/07/the-http1liness-of-http2/</guid>
      </item>
    
      <item>
        <title>In Defence of DOM­Content­Loaded</title>
        <description>&lt;p&gt;Honestly, I started writing this article for no real reason, and somewhat
without context, in December 2022—over half a year ago! But, I left it in
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_drafts/&lt;/code&gt; until today, when a genuinely compelling scenario came up that gives
real opportunity for explanation. It no longer feels like
trivia-for-the-sake-of-it thanks to a recent client project.&lt;/p&gt;

&lt;p&gt;I never thought I’d write an article in defence of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;, but here
it is…&lt;/p&gt;

&lt;p&gt;For many, many years now, performance engineers have been making a concerted
effort to move away from technical metrics such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt;, and toward more
user-facing, UX metrics such as &lt;a href=&quot;https://developer.chrome.com/en/docs/lighthouse/performance/speed-index/&quot;&gt;Speed
Index&lt;/a&gt;
or &lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint/&quot;&gt;Largest Contentful
Paint&lt;/a&gt;.
However, as an internal benchmark, there are compelling reasons why some of you
may actually want to keep tracking these ‘outdated’ metrics…&lt;/p&gt;

&lt;h2 id=&quot;measure-the-user-experience&quot;&gt;Measure the User Experience&lt;/h2&gt;

&lt;p&gt;The problem with using diagnostic metrics like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; to
measure site-speed is that it has no bearing on how a user might actually
experience your site. Sure, if you have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; times of 18 seconds, your site
probably isn’t very fast, but a good &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; time doesn’t mean your site is
necessarily very fast, either.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/dcl.gif&quot; alt=&quot;&quot; width=&quot;904&quot; height=&quot;680&quot; /&gt;
&lt;figcaption&gt;Which do you think provides the better user experience?&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In the comparison above, which do you think provides the better user experience?
I’m willing to bet you’d all say B, right? But, based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;,
A is actually over 11s faster!&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; are internal browser events—your users have no
idea what a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; time even is. I bet half of your colleagues don’t either. As
metrics themselves, they have little to no reflection on the real user
experience, which is exactly why we’ve moved away from them in the first
place—they’re a poor proxy for UX as they’re not emitted when anything useful to
the user happens.&lt;/p&gt;

&lt;p&gt;Or are they…?&lt;/p&gt;

&lt;h2 id=&quot;technically-meaningful&quot;&gt;Technically Meaningful&lt;/h2&gt;

&lt;p&gt;Not all metrics &lt;em&gt;need&lt;/em&gt; to be user-centric. I’m willing to bet you still &lt;a href=&quot;/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/&quot;&gt;monitor
TTFB&lt;/a&gt;, even though
you know your customers will have no concept of a first byte whatsoever. This is
because some metrics are still useful to developers. TTFB is a good measure of
your server response times and general back-end health, and issues here may have
knock-on effects later down the line (namely with Largest Contentful Paint).&lt;/p&gt;

&lt;p&gt;Equally, both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; aren’t just meaningless browser
events, and once you understand what they actually signify, you can get some
real insights as to your site’s runtime behaviour from each of them. Diagnostic
metrics such as these can highlight bottlenecks, and how they might ultimately
impact the user experience in other ways, even if not directly.&lt;/p&gt;

&lt;p&gt;This is particularly true in the case of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-does-it-actually-mean&quot;&gt;What Does It Actually Mean?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;
event&lt;/a&gt;
fires once all of your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript has finished running.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Therefore, anyone leaning heavily on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;—or frameworks that utilise
it—should immediately see the significance of this metric.&lt;/p&gt;

&lt;p&gt;If you aren’t (able to) monitoring custom metrics around your application’s
interactivity, hydration state, etc., then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; immediately
becomes a very useful proxy. Knowing when your main bundles have run is great
insight in lieu of more forensic runtime data, and it’s something I look at with
any client that leans heavily on (frameworks that lean heavily on) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt; or
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=module&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;More accurately, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; signifies that &lt;em&gt;all&lt;/em&gt; blocking &lt;em&gt;and&lt;/em&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=module&lt;/code&gt; code has finished running. We don’t have any
visibility on whether it ran successfully but it has at least finished.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;putting-it-to-use&quot;&gt;Putting It to Use&lt;/h2&gt;

&lt;p&gt;I’m working with a client at the moment who is using &lt;a href=&quot;https://nuxt.com/&quot;&gt;Nuxt&lt;/a&gt;
and has their client-side JavaScript split into an eyewatering 121 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red
files:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/defer-waterfall-abridged.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;522&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;&lt;a href=&quot;/wp-content/uploads/2023/06/defer-waterfall-full.png&quot;&gt;View unabridged.&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Above, the vertical pink line at 12.201s signifies the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event.
That’s late! This client doesn’t have any RUM or custom monitoring in place (&lt;a href=&quot;/sentinel/&quot;&gt;yet&lt;/a&gt;), so, other than Core Web Vitals, we don’t have much
visibility on how the site performs in the wild. Based on a 12s
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event, I can’t imagine it’s doing so well.&lt;/p&gt;

&lt;p&gt;The problem with Core Web Vitals, though, is that its only real JavaScripty
metric, &lt;a href=&quot;https://web.dev/fid/&quot;&gt;First Input Delay&lt;/a&gt;, only deals with user
interaction: what I would like to know is &lt;q&gt;with 121 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red files, when is
there something to actually interact with?!&lt;/q&gt; Based on the lab-based 12s
above, I would love to know what’s happening for real users. And luckily, while
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; is now considered a legacy metric, we can still get field
data for it from two pretty decent sources…&lt;/p&gt;

&lt;h3 id=&quot;chrome-user-experience-report-crux&quot;&gt;Chrome User Experience Report (CrUX)&lt;/h3&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/defer-crux-dashboard.png&quot; alt=&quot;&quot; width=&quot;1800&quot; height=&quot;1360&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Things got a lot worse between March and April 2023&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://developer.chrome.com/docs/crux/dashboard/&quot;&gt;CrUX Dashboard&lt;/a&gt; is one of
very few &lt;a href=&quot;https://developer.chrome.com/docs/crux/&quot;&gt;CrUX resources&lt;/a&gt; that surfaces
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event to us. Above, we can see that, currently, only 11%
of Chrome visitors experience a &lt;em&gt;Good&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;—almost 90% of people
are waiting over 1.5s before the app’s key functionality is available, with
almost half waiting over 3.5s!&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/treo-dcl.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;534&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;&lt;code&gt;DOMContentLoaded&lt;/code&gt; was 4.7s for 75% of Chrome visitors
in May 2023.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It would also seem that &lt;a href=&quot;https://treo.sh/&quot;&gt;Treo&lt;/a&gt; (which is a truly amazing tool)
surfaces &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; data &lt;a href=&quot;https://treo.sh/sitespeed/csswizardry.com?metrics=dcl%2Col&quot;&gt;for a given
origin&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;google-analytics&quot;&gt;Google Analytics&lt;/h3&gt;

&lt;p&gt;Until, well,
&lt;a href=&quot;https://support.google.com/analytics/answer/11583528?hl=en&quot;&gt;today&lt;/a&gt;, Google
Analytics also surfaced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; information. Only this time, we
aren’t limited to just Chrome visits! That said, we aren’t presented with
particularly granular data, either:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/defer-google-analytics.png&quot; alt=&quot;&quot; width=&quot;1726&quot; height=&quot;535&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Huge and non-linear buckets make interrogating the data much more difficult.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;After a bit of adding up (&lt;kbd&gt;2.15 + 10.26 + 45.28 + 25.68 + 13.07&lt;/kbd&gt;
= &lt;samp&gt;96.44&lt;/samp&gt;), we see that the 95th percentile of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;
events for the same time period (May 2023) is somewhere between five and 10
seconds. Not massively helpful, but an insight nonetheless, and at least shows
us that the lab-based 12s is unlikely to be felt by anyone other than extreme
outliers in the field.&lt;/p&gt;

&lt;p&gt;Takeaways here are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Only about 10% of Chrome visitors have what Google deem to be a Good
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/strong&gt; All &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript has run within 1.5s for only
the vast minority of visitors.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3.56% of all users waited over 10s for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/strong&gt; This is a 10
second wait for key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript to run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;small&gt;Given that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event fires after the last of our
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red files has run, there’s every possibility that key functionality from
any preceding files has already become available, but that’s not something we
have any visibility over without looking into custom monitoring, which is
exactly the situation we’re in here. Remember, this is still a proxy metric—just
a much more useful one than you may have realised.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;digging-deeper-the-navigation-timing-api&quot;&gt;Digging Deeper: The Navigation Timing API&lt;/h2&gt;

&lt;p&gt;If we want to capture this data more deliberately ourselves, we need to lean on
the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Navigation_timing&quot;&gt;Navigation Timing
API&lt;/a&gt;,
which gives us access to a suite of milestone timings, many of which you may
have heard of before.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; as measured and emitted by the Navigation Timing API is
actually referred to as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domContentLoadedEventStart&lt;/code&gt;—there is no bare
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domContentLoadedEvent&lt;/code&gt; in that spec. Instead, we have:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domContentLoadedEventStart&lt;/code&gt;:&lt;/strong&gt; This is the one we’re interested in, and is
equivalent to the concept we’ve been discussing in this article so far. To
get the metric we’ve been referring to as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;, you need
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.performance.timing.domContentLoadedEventStart&lt;/code&gt;.
    &lt;ul&gt;
      &lt;li&gt;Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JS is guaranteed to run after synchronous JS, this event
also marks the point that all synchronous work is complete.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domContentLoadedEventEnd&lt;/code&gt;:&lt;/strong&gt; The end event captures the time at which all
JS wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event listener has finished running:
    &lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;DOMContentLoaded&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Do something&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;ul&gt;
      &lt;li&gt;This is separate to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript and runs after our
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event—if we are running a nontrivial amount of code at
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;, we’re also interested in this milestone. That’s not in
the scope of this article, though, so we probably won’t come back to that
again.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Very, very crudely, with no syntactic sugar whatsoever, you can get the page’s
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event in milliseconds with the following:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventStart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigationStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;…and the duration (if any) of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event with:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventEnd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventStart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And of course, we should be very used to seeing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; at the bottom
of DevTools’ &lt;em&gt;Network&lt;/em&gt; panel:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/devtools-dcl.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;813&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;They’re some satisfying numbers.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;even-more-insights&quot;&gt;Even More Insights&lt;/h2&gt;

&lt;p&gt;While &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; tells us when our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red code finished
running—which is great!—it doesn’t tell us how long it took to run. We might
have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; at 5s, but did the code start running at 4.8s? 2s? Who
knows?!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We do.&lt;/strong&gt;&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/defer-waterfall-minimal.png&quot; alt=&quot;&quot; width=&quot;930&quot; height=&quot;522&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;&lt;a href=&quot;/wp-content/uploads/2023/06/defer-waterfall-full.png&quot;&gt;View unabridged.&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In the above waterfall, which is the same one from earlier, only even shorter,
we still have the vertical pink line around 12s, which is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;,
but we also have a vertical sort-of yellow line around 3.5s (actually, it’s at
3.52s exactly). This is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt; is the event
immediately before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domContentLoadedEventStart&lt;/code&gt;. This is the moment the browser
has finished parsing all synchronous DOM work: your HTML and all blocking
scripts it encountered on the way. Basically, the browser is now at the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;/html&amp;gt;&lt;/code&gt; tag. The browser is ready to run your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript.&lt;/p&gt;

&lt;p&gt;One very important thing to note is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt; event fired long,
long before the request for file 133 was even dispatched. Immediately this tells
us that the delta between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; includes code
execution &lt;strong&gt;and any remaining fetch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thankfully, the browser wasn’t just idling in this time. Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red code
runs in sequence, the browser sensibly fetches the files in order and
immediately executes them when they arrive. This level of orchestration is very
elegant and helps to utilise and conserve resources in the most helpful way. Not
flooding the network with responses that can’t yet be used, and also making sure
that the main thread is kept busy.&lt;/p&gt;

&lt;p&gt;This is the JavaScript we need to measure how long our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red activity took:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventStart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domInteractive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, using the Navigation Timing API, we have visibility on when our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red
finished running, and how long it took!&lt;/p&gt;

&lt;p&gt;This demo below contains:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A slow-to-load, fast-to-run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript file.&lt;/li&gt;
  &lt;li&gt;A fast-to-load, slow-to-run inline script set to run at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Logging that out to the console at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Load&lt;/code&gt; event.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- [1] --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://slowfil.es/file?type=js&amp;amp;delay=2000&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;defer&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- [2] --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;DOMContentLoaded&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Hang the browser for 1s at the `DOMContentLoaded` event.&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- [3] --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;performance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigationStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Ready to start running `defer`ed code: &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domInteractive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;`defer`ed code finished: &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventEnd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;`defer`ed code duration: &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventStart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domInteractive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;`DOMContentLoaded`-wrapped code duration: &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventEnd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;timings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domContentLoadedEventStart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/devtools-console.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;813&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;The &lt;q&gt;&lt;code&gt;`defer`ed code finished: 3129ms&lt;/code&gt;&lt;/q&gt; lines up
  with DevTools’ own reported 3.13s &lt;code&gt;DOMContentLoaded&lt;/code&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Or take a look at &lt;a href=&quot;https://deep-bow-engine.glitch.me/&quot;&gt;the live demo on Glitch&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;a-better-way&quot;&gt;A Better Way?&lt;/h2&gt;

&lt;p&gt;This is all genuinely exciting and interesting to me, but we’re running into
issues already:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; is a proxy for when all your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript has run,
but it doesn’t notify you if things ran successfully, or highlight any key
milestones as functionality is constantly becoming available for the duration.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; tells us how long everything took, but that could include
fetch, and there’s no way of isolating the fetch from pure runtime.&lt;/li&gt;
  &lt;li&gt;If you’re capturing these technical timings, you might as well use the User
Timing API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want to expand on the last point.&lt;/p&gt;

&lt;p&gt;If we’re going to go to the effort of measuring Navigation Timing events, we
might as well use the much more useful &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing&quot;&gt;User Timing
API&lt;/a&gt;.
With this, we can emit high-resolution timestamps at arbitrary points in our
application’s lifecycle, so instead of proxying availability via a Navigation
Timing, we can drop, for example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;performance.mark('app booted')&lt;/code&gt; in our code.
In fact, &lt;a href=&quot;https://nextjs.org/docs/pages/building-your-application/optimizing/analytics#custom-metrics&quot;&gt;this is what Next.js
does&lt;/a&gt;
to let you know when the app has hydrated, and how long it took. These User
Timings automatically appear in the &lt;em&gt;Performance&lt;/em&gt; panel:&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/devtools-user-timing.png&quot; alt=&quot;&quot; width=&quot;1500&quot; height=&quot;813&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;performance.mark()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;performance.measure()&lt;/code&gt; in &lt;a href=&quot;https://github.com/csswizardry/csswizardry.github.com/blob/515d5428c1c816a86064739d1a74d77032d520af/_includes/head.html#L115-L118&quot;&gt;a few places on this
site&lt;/a&gt;,
chiefly to monitor how long it takes to parse the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and its CSS.&lt;/p&gt;

&lt;p&gt;The User Timing API is far more suited to this kind of monitoring than something
like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;—I would only look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; if we don’t yet
have appropriate metrics in place.&lt;/p&gt;

&lt;p&gt;Still, the key and most interesting takeaway for me is that if all we have
access to is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; (or we aren’t already using something more
suitable), then we do actually have some visibility on app state and
availability. If you are using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=module&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;
might be more useful to you than you realise.&lt;/p&gt;

&lt;h2 id=&quot;back-to-work&quot;&gt;Back to Work&lt;/h2&gt;

&lt;p&gt;I mentioned previously that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event fires once all
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript has run, which means that we could potentially be
trickling functionality throughout the entire time between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In my client’s case, however, the site is completely nonfunctional until the
very last file (response 133 in the waterfall) has successfully executed. In
fact, blocking the request for file 133 has the exact same effect as disabling
JavaScript entirely. This means the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; event for them is an
almost exact measure of when the app is available. This means that &lt;strong&gt;tracking
and improving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; will have a direct correlation to an improved
customer experience&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;improving-domcontentloaded&quot;&gt;Improving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Given that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; marks the point at which all synchronous HTML and
JavaScript has been dealt with, and all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript has been fetched
and run, this leaves us many different opportunities to improve the metric: we
could reduce the size of our HTML, we could remove or reduce expensive
synchronous JavaScript, we could inline small scripts to remove any network
cost, and we can reduce the amount of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer&lt;/code&gt;red JavaScript.&lt;/p&gt;

&lt;p&gt;Further, as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; is a milestone timing, any time we can shave from
preceding timings should be realised later on. For example, all things being
equal, a 500ms improvement in TTFB will yield a 500ms improvement in subsequent
milestones, such as First Contentful Paint or, in our case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, &lt;em&gt;in our&lt;/em&gt; case, the delta between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domInteractive&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; was 8.681s, or about 70%. And while their TTFB certainly does
need improvement, I don’t think it would be the most effective place to spend
time while tackling this particular problem.&lt;/p&gt;

&lt;p&gt;Almost all of that 8.7s was lost to queuing and fetching that sheer number of
bundles. Not necessarily the size of the bundles—just the sheer quantity of
files that need scheduling, and which each carry their own latency cost.&lt;/p&gt;

&lt;p&gt;While we haven’t worked out the sweet spot for this project, as a rule,
a smaller number of larger bundles would usually download much faster than many
tiny ones:&lt;/p&gt;

&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;As a rule, RTT (α) stays constant while download time (𝑥) is proportional to filesize. Therefore, splitting one large bundle into 16 smaller ones goes from 1α + 𝑥 to 16α + 16(0.0625𝑥). Expect things to probably get a little slower. &lt;a href=&quot;https://t.co/c0hEsIAwKq&quot;&gt;pic.twitter.com/c0hEsIAwKq&lt;/a&gt;&lt;/p&gt;&amp;mdash; Harry Roberts (@csswizardry) &lt;a href=&quot;https://twitter.com/csswizardry/status/1352402710688133122?ref_src=twsrc%5Etfw&quot;&gt;21 January, 2021&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;My advice in this case is to tweak their build to output maybe 8–10 bundles and
re-test from there. It’s important to balance bundle size, number of bundles,
and caching strategies, but it’s clear to me that the issue here is overzealous
code-splitting.&lt;/p&gt;

&lt;p&gt;With that done, we should be able to improve &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt;, thus having
a noticeable impact on functionality and therefore customer experience.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; has proved to be a very, very useful metric for us.&lt;/p&gt;
</description>
        <pubDate>Sat, 01 Jul 2023 00:01:19 +0000</pubDate>
        <link>https://csswizardry.com/2023/07/in-defence-of-domcontentloaded/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/07/in-defence-of-domcontentloaded/</guid>
      </item>
    
      <item>
        <title>Site-Speed Topography Remapped</title>
        <description>&lt;p class=&quot;c-highlight&quot;&gt;&lt;strong&gt;N.B.&lt;/strong&gt; This is an update to my 2020
article &lt;a href=&quot;/2020/11/site-speed-topography/&quot;&gt;&lt;cite&gt;Site-Speed
Topography&lt;/cite&gt;&lt;/a&gt;. You will need to catch up with that piece before this one
makes sense.&lt;/p&gt;

&lt;p&gt;Around two and a half years ago, I debuted my &lt;a href=&quot;/2020/11/site-speed-topography/&quot;&gt;&lt;cite&gt;Site-Speed
Topography&lt;/cite&gt;&lt;/a&gt;
technique for getting broad view of an entire site’s performance from just
a handful of key URLs and some readily available metrics.&lt;/p&gt;

&lt;p&gt;In that time, I have continued to make extensive use of the methodology
(alongside additional processes and workflows), and even other performance
monitoring tools have incorporated it into their own products. Also in that
time, I have adapted and updated the tools and technique itself…&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://csswizardry.gumroad.com/l/site-speed-topography-remapped&quot; class=&quot;btn  btn--full&quot;&gt;Get the new spreadsheet!&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-is-site-speed-topography&quot;&gt;What Is Site-Speed Topography?&lt;/h2&gt;

&lt;p&gt;Firstly, let’s recap the methodology itself.&lt;/p&gt;

&lt;p&gt;The idea is that by taking a handful of representative page- or template-types
from an entire website, we can quickly build the overall landscape—or
&lt;i&gt;topography&lt;/i&gt;—of the site by comparing and contrasting numerical and
milestone timings.&lt;/p&gt;

&lt;p&gt;Realistically, you &lt;em&gt;need&lt;/em&gt; to read &lt;a href=&quot;/2020/11/site-speed-topography/&quot;&gt;the original
post&lt;/a&gt; before this
article will make sense, but the basic premise is that by taking key metrics
from multiple different page types, and analysing the similarities, differences,
or variations among them, you can also very quickly realise some very valuable
insights about other metrics and optimisations you haven’t even captured.&lt;/p&gt;

&lt;p&gt;Pasting a bunch of &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; results into
a spreadsheet is where we start:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2020/10/milestones-spreadsheet.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1614&quot; height=&quot;250&quot; /&gt;
&lt;figcaption&gt;The old &lt;i&gt;Site-Speed Topography&lt;/i&gt; spreadsheet. Plugging milestone
timings into a spreadsheet helps us spot patterns and problems.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Similar
&lt;a href=&quot;/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/&quot;&gt;TTFB&lt;/a&gt;
across pages suggests that no pages have much more expensive database calls than
others; large deltas between TTFB and First Contentful Paint highlight a high
proportion of render-blocking resources; gaps between Largest Contentful Paint
and SpeedIndex suggest late-loaded content. These insights gained across several
representative page types allow us to build a picture of how the entire site
might be built, but from observing only a small slice of it.&lt;/p&gt;

&lt;p&gt;The backbone of the methodology is—or at least &lt;em&gt;was&lt;/em&gt;—viewing the data
graphically and spotting patterns in the bar chart:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2020/10/milestones-chart.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;3080&quot; height=&quot;1452&quot; /&gt;
&lt;figcaption&gt;Viewing the data as a bar chart can help highlight some of the
issues.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Above, we can see that the Product Listing Page (PLP) is by far the worst
performing of the sample, and would need particular attention. We can also see
that First Paint and First Contentful Paint are near identical on all pages
except the PLP—is this a webfont issue? In fact, &lt;a href=&quot;/2020/11/site-speed-topography/#building-the-map&quot;&gt;we can see a lot of
issues&lt;/a&gt;
if we look hard enough. But… who wants to look hard? Shouldn’t these things be
easier to spot?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes&lt;/strong&gt;. They should.&lt;/p&gt;

&lt;h2 id=&quot;remapping&quot;&gt;Remapping&lt;/h2&gt;

&lt;p&gt;If you read the original post, the section &lt;a href=&quot;/2020/11/site-speed-topography/#building-the-map&quot;&gt;&lt;cite&gt;Building the
Map&lt;/cite&gt;&lt;/a&gt;
explains in a lot of words a way to spot visually a bunch of patterns that live
in numbers.&lt;/p&gt;

&lt;p&gt;Surely, if we have all of the facts and figures in front of us anyway, manually
eyeballing a bar chart to try and spot patterns is much more effort than
necessary? We’re already in a spreadsheet—can’t we bring the patterns to us?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes&lt;/strong&gt;. We can.&lt;/p&gt;

&lt;p&gt;Here is the new-look &lt;i&gt;Site-Speed Topography&lt;/i&gt; spreadsheet:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/site-speed-topography-01.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;815&quot; /&gt;
&lt;figcaption&gt;A much more colourful spreadsheet provides way more information. &lt;a href=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/site-speed-topography-01-large.png&quot;&gt;View full size/quality (182KB)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://csswizardry.gumroad.com/l/site-speed-topography-remapped&quot; class=&quot;btn  btn--full&quot;&gt;Get the new spreadsheet!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, without having to do any mental gymnastics at all, we can quickly see:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How pages perform overall across all metrics.&lt;/li&gt;
  &lt;li&gt;Which pages exhibit the best or worst scores for a given metric.&lt;/li&gt;
  &lt;li&gt;General stability of a specific metric.&lt;/li&gt;
  &lt;li&gt;Are any metrics over budget? By how much?
    &lt;ul&gt;
      &lt;li&gt;We can also set thresholds for those budgets.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;We can begin to infer other issues from metrics already present.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, we can still graph the data, but we soon find that that’s almost
entirely redundant now—we solved all of our problems in the numbers.&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/06/site-speed-topography-02.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;726&quot; /&gt;
&lt;figcaption&gt;Graphing the data doesn’t provide as much benefit anymore, but it’s
built into the spreadsheet by default.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Visually, patterns do still emerge: the PD- and SR-Pages have more dense
clusters of bars, suggesting overall worse health; the Home and Product pages
have by far the worst LCP scores; the SRP’s CLS is through the roof. But this is
only visual and not exactly persistent. Still, I have included the chart in the
new spreadsheet because different people prefer different approaches.&lt;/p&gt;

&lt;p&gt;Without looking at a single line of code—without even visiting a single one of
these pages in a browser!—we can already work out where our main liabilities
lie. We know where to focus our efforts, and our day-one to-do list is already
written. No more false starts and dead ends. &lt;strong&gt;Optimise the work not done.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, what are you waiting for? &lt;a href=&quot;https://csswizardry.gumroad.com/l/site-speed-topography-remapped&quot;&gt;Grab a copy of the new Site-Speed Topography
spreadsheet along with a 15-minute screencast
explainer!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://csswizardry.gumroad.com/l/site-speed-topography-remapped&quot; class=&quot;btn  btn--full&quot;&gt;Get the new spreadsheet!&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 07 Jun 2023 17:11:58 +0000</pubDate>
        <link>https://csswizardry.com/2023/06/site-speed-topography-remapped/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/06/site-speed-topography-remapped/</guid>
      </item>
    
      <item>
        <title>Why Not document.write()?</title>
        <description>&lt;!--
  - https://www.webpagetest.org/video/compare.php?tests=230105_BiDcVM_9EX-r:5-c:0,230105_BiDcV4_9EY-r:4-c:0,230105_BiDcJJ_9F0-r:1-c:0,230105_BiDcK7_9F1-r:2-c:0,230105_BiDcH7_9HP-r:1-c:0,230105_BiDc0J_9HQ-r:4-c:0
  --&gt;

&lt;p&gt;If you’ve ever run a Lighthouse test before, there’s a high chance you’ve seen
the audit &lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/best-practices/no-document-write/&quot;&gt;&lt;q&gt;Avoid
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;&lt;/q&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;/wp-content/uploads/2023/01/lighthouse.png&quot; alt=&quot;&quot; width=&quot;1396&quot; height=&quot;461&quot; /&gt;
&lt;figcaption&gt;&lt;q&gt;For users on slow connections, external scripts dynamically
injected via &lt;code&gt;document.write()&lt;/code&gt; can delay page load by tens of
seconds.&lt;/q&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;You may have also seen that there’s very little explanation as to &lt;em&gt;why&lt;/em&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; is so harmful. Well, the short answer is:&lt;/p&gt;

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

&lt;p&gt;&lt;small&gt;&lt;strong&gt;N.B.&lt;/strong&gt; This audit and, accordingly, this article, only deals with
script injection using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;—not its usage in general. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/write&quot;&gt;The MDN
entry for
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;&lt;/a&gt;
does a good job of discouraging its use.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-makes-scripts-slow&quot;&gt;What Makes Scripts Slow?&lt;/h2&gt;

&lt;p&gt;There are a number of things that can make regular, synchronous scripts&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
slow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Synchronous JS &lt;strong&gt;can&lt;/strong&gt; block DOM construction while the file is downloading.
    &lt;ul&gt;
      &lt;li&gt;The belief that &lt;q&gt;synchronous JS blocks DOM construction&lt;/q&gt; is only true
in certain scenarios.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Synchronous JS &lt;strong&gt;always&lt;/strong&gt; blocks DOM construction while the file is
executing.
    &lt;ul&gt;
      &lt;li&gt;It runs in-situ at the exact point it’s defined, so anything defined after
the script has to wait.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Synchronous JS &lt;strong&gt;never&lt;/strong&gt; blocks downloads of subsequent files.
    &lt;ul&gt;
      &lt;li&gt;This has been true for &lt;a href=&quot;https://www.stevesouders.com/blog/2008/03/10/ie8-speeds-things-up/&quot;&gt;almost 15 years&lt;/a&gt;
at the time of writing, yet still remains a common misconception among
developers. This is closely related to the first point.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;,
however, forces scripts into both (1) and (2) regardless of when they’re
defined.&lt;/p&gt;

&lt;h2 id=&quot;the-preload-scanner&quot;&gt;The Preload Scanner&lt;/h2&gt;

&lt;p&gt;The reason scripts never block subsequent downloads is because of something
called the &lt;em&gt;Preload Scanner&lt;/em&gt;. 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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; element while the file
is downloading if the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; element is early in the HTML. Late (e.g.
at-&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;) synchronous &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;s are more likely to be fetched by the
Preload Scanner while the primary parser is still hung up doing work earlier in
the page.&lt;/p&gt;

&lt;p&gt;Put simply, scripts defined earlier in the page are more likely to block on
their download than later ones; later scripts are more likely to have been
fetched preemptively and asynchronously by the Preload Scanner.&lt;/p&gt;

&lt;h2 id=&quot;documentwrite-hides-files-from-the-preload-scanner&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; Hides Files From the Preload Scanner&lt;/h2&gt;

&lt;p&gt;Because the Preload Scanner deals with tokeniseable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href&lt;/code&gt; attributes,
anything buried in JavaScript is invisible to it:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;script src=file.js&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/script&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block
that inserts it, which is very much just-in-time (and too late).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; forces scripts to block DOM construction during their
download by hiding them from the Preload Scanner.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;what-about-async-snippets&quot;&gt;What About Async Snippets?&lt;/h3&gt;

&lt;p&gt;Async snippets such as the one below suffer the same fate:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;file.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file.js&lt;/code&gt; is not a filepath—it’s a string! It’s not until the browser has
run this script that it puts a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;That said, &lt;a href=&quot;/2022/10/speeding-up-async-snippets/&quot;&gt;async snippets are still an
anti-pattern&lt;/a&gt;—don’t use them.&lt;/p&gt;

&lt;h2 id=&quot;documentwrite-executes-synchronously&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; Executes Synchronously&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; is almost exclusively used to conditionally load
a synchronous script. If you just need &lt;strong&gt;a blocking script&lt;/strong&gt;, you’d use a simple
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; element:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;file.js&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you needed to &lt;strong&gt;conditionally load an asynchronous script&lt;/strong&gt;, you’d add
some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;else&lt;/code&gt; logic to your async snippet.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;file.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you need to &lt;strong&gt;conditionally load a synchronous script&lt;/strong&gt;, you’re kinda stuck…&lt;/p&gt;

&lt;p&gt;Scripts injected with, for example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appendChild&lt;/code&gt; are, per the spec,
asynchronous. If you need to inject a synchronous file, one of the only
straightforward options is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;script src=file.js&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/script&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; forces scripts to block DOM construction during their
execution by being synchronous by default.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;is-it-all-bad&quot;&gt;Is It All Bad?&lt;/h2&gt;

&lt;p&gt;The location of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; in question makes a huge difference.&lt;/p&gt;

&lt;p&gt;Because the Preload Scanner works most effectively when it’s dealing with
subresources later in the page, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; earlier in the HTML is less
harmful.&lt;/p&gt;

&lt;h3 id=&quot;early-documentwrite&quot;&gt;Early &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  ...

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;script src=https://slowfil.es/file?type=js&amp;amp;delay=1000&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/script&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;stylesheet&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://slowfil.es/file?type=css&amp;amp;delay=1000&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  ...

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you put a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; as the very first thing in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, it’s
going to behave the exact same as a regular &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;—the Preload Scanner
wouldn’t have had much of a head start anyway, so we’ve already missed out on
the chance of an asynchronous fetch:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/01/document.write-early.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;131&quot; /&gt;
&lt;figcaption&gt;&lt;code&gt;document.write()&lt;/code&gt; as the first thing in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. &lt;a href=&quot;https://www.webpagetest.org/result/230105_BiDcK7_9F1/2/details/&quot;&gt;FCP is at 2.778s.&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Above, we see that the browser has managed to parallelise the requests: the
primary parser ran and injected the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;, while the Preload
Scanner fetched the CSS.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Owing to CSS’ &lt;em&gt;Highest&lt;/em&gt; priority, it will always be requested before
&lt;em&gt;High&lt;/em&gt; priority JS, regardless of where each is defined.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;If we replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; with a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;, we’d see the
exact same behaviour, meaning in this specific instance, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; is
no more harmful than a regular, synchronous script:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  ...

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://slowfil.es/file?type=js&amp;amp;delay=1000&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;stylesheet&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://slowfil.es/file?type=css&amp;amp;delay=1000&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  ...

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This yields an identical waterfall:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/01/sync.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;131&quot; /&gt;
&lt;figcaption&gt;Using a syncrhonous &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; instead of &lt;code&gt;document.write()&lt;/code&gt;. &lt;a href=&quot;https://www.webpagetest.org/result/230105_BiDcV4_9EY/4/details/&quot;&gt;FCP is at 2.797s.&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Because the Preload Scanner was unlikely to find either variant, we don’t
notice any real degradation.&lt;/p&gt;

&lt;h3 id=&quot;late-documentwrite&quot;&gt;Late &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;

  ...

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;stylesheet&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://slowfil.es/file?type=css&amp;amp;delay=1000&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;script src=https://slowfil.es/file?type=js&amp;amp;delay=1000&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/script&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  ...

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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, &lt;a href=&quot;/2018/11/css-and-network-performance/#dont-place-link-relstylesheet--before-async-snippets&quot;&gt;CSS
blocks
JS&lt;/a&gt;,
and in this example, serves to hide the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; from the Preload
Scanner.&lt;/p&gt;

&lt;p&gt;Thus, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; 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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;As soon as we hide the script file from the Preload Scanner, we notice
drastically different behaviour. By simply swapping the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; and
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rel=stylesheet&lt;/code&gt; around, we get a much, much slower experience:&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/01/document.write.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;131&quot; /&gt;
&lt;figcaption&gt;&lt;code&gt;document.write()&lt;/code&gt; late in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. &lt;a href=&quot;https://www.webpagetest.org/result/230105_BiDcVM_9EX/5/details/&quot;&gt;FCP is at 4.073s.&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Now that we’ve hidden the script from the Preload Scanner, we lose all
parallelisation and incur a much larger penalty.&lt;/p&gt;

&lt;h2 id=&quot;it-gets-worse&quot;&gt;It Gets Worse…&lt;/h2&gt;

&lt;p&gt;The whole reason I’m writing this post is that I have a client at the moment who
is using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; late in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. 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!&lt;/p&gt;

&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2023/01/cpu.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;229&quot; /&gt;
&lt;figcaption&gt;The main thread goes completely silent during the injected file’s
fetch. This doesn’t happen when files are fetched from the Preload Scanner.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;avoid-documentwrite&quot;&gt;Avoid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;As well as exhibiting unpredictable and buggy behaviour as keenly stressed in
the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/write&quot;&gt;MDN&lt;/a&gt; and
&lt;a href=&quot;https://developer.chrome.com/blog/removing-document-write/&quot;&gt;Google articles&lt;/a&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.write()&lt;/code&gt; (but at least now you know why).&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script src=&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Tue, 10 Jan 2023 16:17:11 +0000</pubDate>
        <link>https://csswizardry.com/2023/01/why-not-document-write/</link>
        <guid isPermaLink="true">https://csswizardry.com/2023/01/why-not-document-write/</guid>
      </item>
    
  </channel>
</rss>
