<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/css" href="/c/feed-styles-bQufdv2m.css"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://jakearchibald.com/</id>
    <title>Jake Archibald's blog</title>
    <updated>2025-11-27T12:07:43.195Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://jakearchibald.com/"/>
    <logo>https://jakearchibald.com/c/icon-CJ5sT8Gh.png</logo>
    <icon>https://jakearchibald.com/favicon.ico</icon>
    <rights>https://creativecommons.org/licenses/by/4.0/</rights>
    <entry>
        <title type="html"><![CDATA[Importing vs fetching JSON]]></title>
        <id>https://jakearchibald.com/2025/importing-vs-fetching-json/</id>
        <link href="https://jakearchibald.com/2025/importing-vs-fetching-json/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-WUSWKqXB.png" type="image/png"/>
        <updated>2025-10-22T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[They behave differently, so make sure you pick the right one.]]></summary>
        <content type="html"><![CDATA[<p>This year, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with">JSON module imports became baseline &#39;newly available&#39;</a>, meaning they&#39;re implemented across browser engines.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./data.json'</span> <span class="token keyword">with</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>

<span class="token comment">// And…</span>

<span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> data <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>I&#39;m glad JavaScript has this feature, but I can&#39;t see myself using it in a browser environment, other than small demos. I might still use it in frontend source code, but generally only in cases where it&#39;d be bundled away before reaching the browser.</p>
<p>It comes down to the behaviour differences compared to this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Here&#39;s why…</p>
<h2 id="error-handling"><a href="#error-handling">Error handling</a></h2>
<p>With a static import:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./data.json'</span> <span class="token keyword">with</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre></div><p>If the above fails, it takes the whole module graph down with it. Because of this, I&#39;d never use this pattern with some third-party JSON, as I&#39;d want to be able to provide a fallback if the third-party service fails.</p>
<p>But <code>import()</code> allows exactly that:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">try</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> data <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
    <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// Fallback logic</span>
<span class="token punctuation">}</span></code></pre></div><p>This is pretty good. Although the <code>fetch()</code> alternative:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">try</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// Fallback logic</span>
<span class="token punctuation">}</span></code></pre></div><p>…allows much more introspection in event of a failure. There&#39;s <code>response.status</code>, or you can use <code>response.text()</code>, meaning you still have the source if JSON parsing fails. Maybe that doesn&#39;t matter in all cases.</p>
<p>I think the bigger issue is…</p>
<h2 id="caching-and-garbage-collection"><a href="#caching-and-garbage-collection">Caching and garbage collection</a></h2>
<p>When you import a module (be it JS, WASM, CSS, or JSON), it&#39;s cached for the lifetime of the environment (e.g. a page or worker), even if the result is a network or parsing failure. All imports for a given specifier &amp; type return the same module.</p>
<p>This is generally a good thing, as it means your JS module returns the same objects every time, and state can be shared across all importers. But if you&#39;re doing something like:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> results <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'/api/search?q=whatever'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>…then you have a memory leak, because each set of search results will live in the module graph for the life of the page. That isn&#39;t the case with <code>fetch()</code>, where returned objects can be garbage collected once they&#39;re out of reference.</p>
<p>The same applies to cases like:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">let</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> largeData <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'/large-data.json'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">const</span> someSmallPart <span class="token operator">=</span> largeData<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
largeData <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span></code></pre></div><p>If the above used <code>fetch()</code>, then the data other than <code>someSmallPart</code> could be garbage collected. But with <code>import()</code>, the whole <code>largeData</code> object remains in memory for the life of the page.</p>
<h2 id="when-should-json-modules-be-used"><a href="#when-should-json-modules-be-used">When should JSON modules be used?</a></h2>
<p>It makes sense to use JSON module imports for local static JSON resources where you need all/most of the data within. Particularly since bundlers can understand JSON imports, and bundle the object with other modules. That isn&#39;t possible with <code>fetch()</code>, unless you use some pretty hacky plugins.</p>
<p>In server code, I might import <code>package.json</code> to get the version number. However, I wouldn&#39;t do this with frontend code, as it&#39;s wasteful to bundle all of <code>package.json</code> just to get a single value – bundlers don&#39;t perform tree-shaking of individual object keys.</p>
<p><strong>Update:</strong> <a href="https://mastodon.social/@jed/115418637695312552">Jed</a> and <a href="https://bsky.app/profile/uncenter.dev/post/3m3sb5ihaks2g">leah</a> point out, many bundlers (esbuild, Vite, and Rollup at least) will expose top level keys as individual exports if you use their non-standard JSON import syntax.</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// No treeshaking</span>
<span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./package.json'</span> <span class="token keyword">assert</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// No treeshaking</span>
<span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./package.json'</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Treeshaking works!</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> version <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./package.json'</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This only tree-shakes the top-level keys, so if your data is deeply nested, you&#39;re still bundling more than you need. Generally, I try to turn any &quot;fetch-and-process&quot; logic into a Vite/Rollup plugin, so it happens at build time rather than runtime.</p>
<p>I&#39;m glad native JSON importing feature exists, but it should be used with care, and not as a blanket replacement for <code>fetch()</code>ing JSON.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[The present and potential future of progressive image rendering]]></title>
        <id>https://jakearchibald.com/2025/present-and-future-of-progressive-image-rendering/</id>
        <link href="https://jakearchibald.com/2025/present-and-future-of-progressive-image-rendering/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-ClfgDFJb.jpg" type="image/jpg"/>
        <updated>2025-10-15T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[Exploring progressive image rendering across JPEG, PNG, WebP, AVIF, and JPEG XL.]]></summary>
        <content type="html"><![CDATA[<p>Progressive image formats allow the decoder to create a partial rendering when only part of the image resource is available. Sometimes it&#39;s part of the image, and sometimes it&#39;s a low quality/resolution version of the image. I&#39;ve been digging into it recently, and I think there are some common misconceptions, and I&#39;d like to see a more pragmatic solution from AVIF.</p>
<p>Let&#39;s dive straight into the kinds of progressive rendering that are currently available natively in browsers.</p>
<h2 id="the-current-state-of-progressive-image-rendering-in-browsers"><a href="#the-current-state-of-progressive-image-rendering-in-browsers">The current state of progressive image rendering in browsers</a></h2>
<p>Here&#39;s the image I&#39;m going to use for comparison, and you&#39;ll be sick of seeing it by the end of the article:</p>
<figure class="full-figure max-figure">
  <img src="/c/fox-DSbxk4jA.avif" loading="lazy" width="1598" height="1753" alt="A winter photo of a fox, standing on grass, with foliage in the background" style="height:auto" />
</figure>

<p>Due to its high resolution (1598x1753), and sharp detail, it&#39;s 155 kB, even using AVIF.</p>
<p>I created similarly file-sized versions of the image in other formats to compare when they start to render, and when they show enough detail to identify it as a fox in woodland</p>
<p>Let&#39;s go!</p>
<h3 id="jpeg"><a href="#jpeg">JPEG</a></h3>
<p>Trusty old JPEG can be encoded in a progressive way, which is the default in <a href="https://squoosh.app/">Squoosh</a>.</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/">Here&#39;s a little web app</a> that lets you test progressive rendering of any image, and here&#39;s the <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.jpg&density=2">app ready-loaded with the JPEG version of the fox image</a>.</p>
<p>You get a top-to-bottom low resolution render that starts around 1.2 kB in, and it finishes its first pass at around 33 kB (21% of the file size). Interestingly, that looks different in Firefox and Chromium-based browsers compared to Safari:</p>
<figure class="full-figure max-figure">
  <img src="/c/jpeg-safari-vs-Br8Dbgrp.avif" loading="lazy" width="1598" height="1721" alt="The previous fox image, partially loaded. In Safari it appears blocky, in Firefox and Chromium it appears smooth" style="height:auto" />
</figure>

<p>In both cases you can tell what the image is, but the Safari rendering is blocky, whereas Firefox and Chromium are smoothed. I much prefer the Firefox/Chromium rendering.</p>
<p>After that, the resolution is improved over multiple passes.</p>
<p>With JPEG, progressive rendering seems to be free in terms of quality/size, but it isn&#39;t free in terms of decoding time. <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/">Here&#39;s another little web app</a>, this one benchmarks image decoding in the browser. Comparing <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.jpg">progressive JPEG</a> with <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox-no-progressive.jpg">non-progressive JPEG</a>, the progressive JPEG takes around 40% longer to decode, but on my M4 pro that&#39;s only about 1.3ms, which seems like a reasonable trade-off.</p>
<h3 id="png"><a href="#png">PNG</a></h3>
<p>PNG generally renders from top to bottom. There&#39;s an interlaced mode, but it significantly increases the file size, so I don&#39;t recommend it.</p>
<p>For completion, <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-interlaced.png&density=2">here&#39;s a demo</a>. I had to… umm… modify the image somewhat, in order to bring the file size down to some acceptable level.</p>
<h3 id="webp"><a href="#webp">WebP</a></h3>
<p>In Firefox and Chrome, WebP renders from top to bottom. It also takes around 43 kB (28%) before it renders anything. I guess the start of the file contains the colour data, so it can only render progressively as it starts receiving the final luma channel.</p>
<p>Unfortunately, Safari doesn&#39;t perform any progressive rendering of WebP. It doesn&#39;t render anything until it has the whole file.</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.webp&density=2">Here&#39;s a demo</a>.</p>
<h3 id="avif"><a href="#avif">AVIF</a></h3>
<p>Regular AVIF doesn&#39;t support any kind of progressive rendering. You get nothing, then you get the whole image (<a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.avif&density=2">demo</a>).</p>
<p>But, AVIF does have a little-known progressive rendering feature! I couldn&#39;t get good results with the <code>--progressive</code> flag it offers, but I got something interesting with the lower level <code>--layered</code> flag. It&#39;s experimental, and I had to compile libavif myself to get it working. Here&#39;s the command I used:</p>
<div class="code-example"><pre>avifenc -s 0 -y 420 --layered \
  --scaling-mode:u 1/8 -q:u 0 fox.png \
  --scaling-mode:u 1 -q:u 16 fox.png \
  fox-progressive.avif</pre></div><p>This creates two layers, one at 1/8th resolution and &#39;minimum&#39; quality, and one at full resolution and good quality. These layers work similarly to frames of a video, with the second layer using data from the first to save space (a P-frame in video terms).</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-progressive.avif&density=2">Here&#39;s a demo</a>. It only progressively renders in Chromium browsers. You get two renders: the full image when all the data has been received, and an earlier render at ~5.8 kB (4% of the data):</p>
<style>
.async-content {
  position: relative;
}
@keyframes async-content-fade-in {
  from { opacity: 0; animation-timing-function: ease-in-out; }
}
.async-content-loading {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, .62);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: 300ms async-content-fade-in;
}
.async-content-loading loading-spinner {
  --color: #fff;
}
</style><style>
.tabs-component {
  display: flex;
  flex-flow: row wrap;
}
.tabs-component-tab {
  flex: 1;
  min-width: 70px;
  display: grid;
}
.tabs-component input[type=radio] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.tabs-component-label {
  border-top: 7px solid #ffe454;
  padding: 0.7em 0.7em;
  text-align: center;
  cursor: pointer;
  line-height: 1.3;
  font-size: 0.9rem;
}
input[type=radio]:checked + .tabs-component-label {
  background: #ffe454;
}
input[type=radio]:focus-visible + .tabs-component-label {
  background: #b9b9b9;
}
input[type=radio]:focus-visible:checked + .tabs-component-label {
  background: #ffc254;
}
</style><style>
.image-tabs {
  position: relative;
}
.image-tabs-pop-out-icon {
  position: absolute;
  top: 10px;
  right: 10px;
  fill: #fff;
  width: 32px;
  filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.7));
}
.image-tabs-preview {
  position: relative;
  overflow: hidden;
  background: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path d="M1 2V0h1v1H0v1z" fill-opacity=".025"/></svg>');
  background-size: 20px 20px;
}
.image-tabs-background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0 auto;
}
.image-tabs-transformer {
  position: relative;
}
.image-tabs-transformer img,
.image-tabs-transformer canvas {
  position: absolute;
  top: 0;
  left: 50%;
  width: 100%;
  height: 100%;
  margin: 0 auto;
  transform: translateX(-50%);
}
</style>

<figure class="full-figure max-figure">
<div class="post-component-2"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">5.8 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Full image (151 kB)</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-BhXDY77h.js';import Component from '/c/ImageTabs-MrFnKNkF.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["5.8 kB","/c/initial-pass-Dx0jAW22.avif"],["Full image (151 kB)","/c/fox-progressive-CU6pVNDH.avif"]]}), document.querySelector('.post-component-2'));</script>
</figure>

<p>It looks… bad, but you can tell what it is, and it&#39;s pretty good for ~5.8 kB.</p>
<p>The downside is, something odd happens with the quality option when you use <code>--layered</code> mode. It doesn&#39;t equate to the same numbers as normal AVIF encoding.</p>
<p>Also, there seems to be some overhead in terms of file size, but it&#39;s hard to measure. From playing around with MS-SSIM, it seems to be in the region of 5-6 kB, so… the size of the initial pass.</p>
<p>The configuration of layers I used is just one example, but it seems pretty limited. Only particular scaling values are allowed, and 1/8 is the smallest. Supposedly, additional layers are possible, allowing for extra steps in the progressive render, but whenever I tried this, the encoder would error out, or explode the file size to ~400 kB, even at lowest quality. I guess that&#39;s why it&#39;s marked &#39;experimental&#39;.</p>
<p>In terms of decoding time, here&#39;s <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox-progressive.avif">progressive AVIF</a> vs <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.avif">regular AVIF</a>. In Firefox and Safari, the decoding time for progressive is about 1-2% longer. In Chrome it takes ~40% longer, which is 3.5ms on my M4 Pro. My guess is that Chrome is missing a fast-path for cases where it doesn&#39;t need to render the first layer.</p>
<p>Here&#39;s <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1712813">the issue for tracking progressive rendering of AVIF in Firefox</a>.</p>
<h3 id="jpeg-xl"><a href="#jpeg-xl">JPEG XL</a></h3>
<p>If you ask web developers why they want JPEG XL (as I did on <a href="https://bsky.app/profile/jakearchibald.com/post/3m2mcuqjekc2a">bluesky</a> and <a href="https://mastodon.social/@jaffathecake/115333326583200558">mastodon</a>), almost everyone mentions progressive rendering. Given that, I was surprised that Safari (the only browser to support JPEG XL) doesn&#39;t perform any kind of progressive rendering. <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-progressive.jxl&density=2">Here&#39;s a demo</a>, and <a href="https://bugs.webkit.org/show_bug.cgi?id=272350">a bug report</a>.</p>
<p>I was also surprised to see that, in Safari, <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.jxl">JPEG XL takes 150% longer</a> (as in 2.5x) to decode vs <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.avif">an equivalent AVIF</a>. That&#39;s 17ms longer on my M4 Pro. Apple hardware tends to be high-end, but this could still be significant. This isn&#39;t related to progressive rendering; the decoder is just slow. <a href="https://www.reddit.com/r/jpegxl/comments/1djcccj/comment/l9at8p3/">There&#39;s some suggestion</a> that the Apple implementation is running on a single core, so maybe there&#39;s room for improvement.</p>
<p>JPEG XL support in Safari actually comes from the underlying OS rather than the browser. My guess is that Apple is considering using JPEG XL for iPhone photo storage rather than HEIC, and JPEG XL&#39;s inclusion in the browser is a bit of an afterthought. I&#39;m just guessing though.</p>
<p>The implementation that was in Chromium behind a flag <em>did</em> support progressive rendering to some degree, but it didn&#39;t render anything until ~60 kB (39% of the file). The rendering is similar to the initial JPEG rendering above, but takes much more image data to get there. This is a weakness in the decoder rather than the format itself. I&#39;ll dive into what JPEG XL is capable of shortly.</p>
<p>I also tested the performance of the old behind-a-flag Chromium JPEG XL decoder, and it&#39;s over 500% slower (6x) to decode than AVIF. The old behind-a-flag Firefox JPEG XL decoder is about as slow as the Safari decoder. It&#39;s not fair to judge the performance of experimental unreleased things, but I was kinda hoping one of these would suggest that the Safari implementation was an outlier.</p>
<p>I thought that &quot;fast decoding&quot; was one of the selling points of JPEG XL over AVIF, but now I&#39;m not so sure.</p>
<p><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1986393">We have a Rust implementation of JPEG XL underway in Firefox</a>, but performance needs to get a lot better before we can land it.</p>
<h2 id="but-do-we-actually-benefit-from-progressive-rendering"><a href="#but-do-we-actually-benefit-from-progressive-rendering">But do we actually benefit from progressive rendering?</a></h2>
<figure class="full-figure max-figure">
  <img src="/c/f1-DK8ANwoR.avif" loading="lazy" width="1598" height="1478" alt="A old Williams F1 car speeding up a hill" style="height:auto" />
</figure>

<p>Thanks to modern image formats (AVIF in this case), the above image is 56.4 kB. That&#39;s pretty incredible for a more-than-HD image. The fox image is much larger because of its sharp details, but that isn&#39;t true for most images. This means that, for many images, there&#39;s little benefit to progressive rendering.</p>
<p>But even with the 155 kB fox image, <a href="https://mastodon.social/@kornel/115339016909614657">Kornel makes a good point</a>:</p>
<blockquote class="quote"><p>Congestion and bufferbloat make data arrive in laggy bursts rather than slowly. Very bad signal strength also tends to be on/off.</p></blockquote>

<p>This means that even if it takes 5 seconds to receive the image, it&#39;s unlikely that 155 kB will be received gradually throughout that period. I&#39;m currently on aeroplane WiFi, and yeah… most of the slowness is while <em>nothing</em> is being received.</p>
<p>That means seeing a progressive render requires a particular mix of good and bad luck:</p>
<ul>
<li>Bad luck: your connection isn&#39;t great, so it&#39;s taking a while to fetch the whole image.</li>
<li>Good luck: a portion of the image has been received.</li>
</ul>
<p>Progressive rendering is a good experience when you hit this (not-)sweet spot, but you might not get to see it even on slow connections. It certainly isn&#39;t an alternative to taking care over your image sizes.</p>
<p>Because of this, it feels like encoding an image in a way that enables a progressive render should be minimal cost in terms of file size overhead, and minimal/zero cost in decoding overhead in cases where progressive isn&#39;t needed.</p>
<p>I know that&#39;s a weak &#39;story&#39;. When I set about writing this article, I intended it to be a strong argument for progressive rendering. But after digging into it, my feelings are less certain.</p>
<h2 id="what-about-progressive-rendering-instead-of-responsive-images"><a href="#what-about-progressive-rendering-instead-of-responsive-images">What about progressive rendering instead of responsive images?</a></h2>
<p>When I asked folks why they&#39;re interested in JPEG XL, some folks suggested it would enable them to replace responsive images with a single maximum-resolution JPEG XL file. The idea is:</p>
<ol>
<li>The browser receives some image data.</li>
<li>The browser performs a partial decode of the image data.</li>
<li>The browser realises it doesn&#39;t need any more pixel density.</li>
<li>The browser tells the server &quot;stop sending me this resource&quot;.</li>
<li>The server dutifully stops sending the resource.</li>
</ol>
<p>I&#39;m sorry to be the bad news guy, but I&#39;m pretty certain <em>this won&#39;t work</em>.</p>
<p>The problem is there&#39;s significant lag between each of these steps, to the point where it&#39;s very likely that you&#39;ll receive megabytes of data you don&#39;t need, per image, before the response is successfully cancelled.</p>
<p>It might be workable along with a new browser feature, where the ranges are known up-front, so the browser can make the required partial request:</p>
<div class="code-example"><pre class="language-html"><code><span class="token comment">&lt;!-- Made-up example --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span>
  <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>
    img.jxl#range=0-10000   200w,
    img.jxl#range=0-30000   400w,
    img.jxl#range=0-70000   800w,
    img.jxl#range=0-150000 1600w
  <span class="token punctuation">"</span></span>
  <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…as usual…<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span></code></pre></div><p>But even if we get this <code>#range=</code> feature (and there are some big questions around CORS that could make it a no-go), the complexity is about the same as responsive images today, and with responsive images you get more control over the available resolutions.</p>
<p>I guess an advantage of the &#39;range&#39; approach is the browser could &#39;resume&#39; the image download if it later finds it needs a higher resolution, such as going from thumbnail to full-screen. But even given that, current responsive images seem more versatile.</p>
<h2 id="potential-progressive-rendering-with-jpeg-xl"><a href="#potential-progressive-rendering-with-jpeg-xl">Potential progressive rendering with JPEG XL</a></h2>
<p>Aside from browser decoders, there&#39;s some promising work in progressive rendering of JPEG XL. However, it&#39;s unclear how much of it could make it into browsers, and the encoder settings are a little confusing.</p>
<p>The current version of cjxl (0.11.1) has a <code>--progressive</code> flag, but this creates a file that can&#39;t be rendered until a significant amount of the file has been received (39% of the file with the fox image).</p>
<p>To get an earlier render, you need to use <code>--progressive_dc 1</code>, which <code>cjxl --help</code> doesn&#39;t mention. You only get to find out about it if you use the verbose flag, twice: <code>cjxl --help -v -v</code>. It seems like this has been changed in the codebase, where <a href="https://github.com/libjxl/libjxl/commit/62712477eec764c4e730f94476113f6e4756dfb2"><code>--progressive</code> now implies <code>progressive_dc 1</code></a>, but that hasn&#39;t made it into a release yet.</p>
<p>It doesn&#39;t seem like <code>progressive_dc</code> has an impact on file size. If it does, it&#39;s small. I haven&#39;t seen a difference in decode time either (from testing in Safari), but JPEG XL decoding is slow in general, so it isn&#39;t a huge win.</p>
<p>djxl doesn&#39;t support the earlier rendering that <code>--progressive_dc</code> offers, but there&#39;s also a Rust-based decoder, <a href="https://github.com/tirr-c/jxl-oxide">jxl-oxide</a>, which can handle earlier rendering.</p>
<p>jxl-oxide isn&#39;t fast enough for browsers, but it&#39;s an interesting preview of what&#39;s possible. There&#39;s also a <a href="https://jxl-oxide.tirr.dev/demo/index.html">neat wasm version, that lets you see partial renders</a>. Here&#39;s a <a href="/c/fox-progressive-wBqOQbxQ.jxl">progressive version of the fox image</a>, if you want to try it yourself. Otherwise, here&#39;s a rough guide:</p>
<figure class="full-figure max-figure">
<div class="post-component-3"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">2 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">3 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">4.25 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="3"/><span class="tabs-component-label">6 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="4"/><span class="tabs-component-label">10 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="5"/><span class="tabs-component-label">17 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="6"/><span class="tabs-component-label">27 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="7"/><span class="tabs-component-label">46 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="8"/><span class="tabs-component-label">60 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="9"/><span class="tabs-component-label">68 kB</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-BhXDY77h.js';import Component from '/c/ImageTabs-MrFnKNkF.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["2 kB","/c/2000-D6ESPIzI.avif"],["3 kB","/c/3000-B1joeRWz.avif"],["4.25 kB","/c/4250-YeB75_4X.avif"],["6 kB","/c/6000-CL0crpuE.avif"],["10 kB","/c/10000-032SbDmD.avif"],["17 kB","/c/17000-B4rt11Xh.avif"],["27 kB","/c/27000-Cl4PZSI1.avif"],["46 kB","/c/46000-Da3ZvuEo.avif"],["60 kB","/c/60000-Db0PUBzp.avif"],["68 kB","/c/68000-J7o32Hc6.avif"]]}), document.querySelector('.post-component-3'));</script>
</figure>

<p>I&#39;d say that the 6 kB mark gives you a decent impression of the image, and things get really clear around 46 kB. It could use some smoothing, like we saw with the JPEG progressive render in Firefox/Chrome.</p>
<p>After the 60 kB mark, the full detail appears in square blocks. A nice feature of JPEG XL is these can appear in any order, so you can see in the 68 kB render that sharpness appears around the fox&#39;s face before anywhere else. cjxl lets you specify a point to use as the &quot;center&quot;, but a smarter encoder could detect things like faces and prioritise them.</p>
<p>It&#39;s subjective, but I&#39;d say the 5.8 kB AVIF progressive render from earlier is, detail-wise, somewhere between the 17-27 kB JPEG XL renders, but make up your own mind:</p>
<figure class="full-figure max-figure">
<div class="post-component-4"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" value="0"/><span class="tabs-component-label">JXL 6 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">JXL 10 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">JXL 17 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="3"/><span class="tabs-component-label">AVIF 5.8 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="4"/><span class="tabs-component-label">JXL 27 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="5"/><span class="tabs-component-label">JXL 46 kB</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-BhXDY77h.js';import Component from '/c/ImageTabs-MrFnKNkF.js';render(h(Component, {"ratio":0.91158015,"initial":3,"images":[["JXL 6 kB","/c/6000-CL0crpuE.avif"],["JXL 10 kB","/c/10000-032SbDmD.avif"],["JXL 17 kB","/c/17000-B4rt11Xh.avif"],["AVIF 5.8 kB","/c/initial-pass-Dx0jAW22.avif"],["JXL 27 kB","/c/27000-Cl4PZSI1.avif"],["JXL 46 kB","/c/46000-Da3ZvuEo.avif"]]}), document.querySelector('.post-component-4'));</script>
</figure>

<p>So if the browser manages to download 6 kB then stalls, then AVIF can produce a better result. But since the AVIF rendering is only two-pass, if the browser has say 50 kB, then the JPEG XL rendering is much better.</p>
<p>This is all just &#39;in theory&#39; until it becomes fast enough to land in a browser.</p>
<p>There&#39;s another JPEG XL rust decoder jxl-rs, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1986393">being developed for Firefox</a>. That team are also exploring how progressive rendering could work.</p>
<h2 id="a-better-progressive-feature-for-avif"><a href="#a-better-progressive-feature-for-avif">A better &#39;progressive&#39; feature for AVIF?</a></h2>
<p>This article got longer than I expected, but one last thing…</p>
<p>In my experience AVIF results in smaller files at what I&#39;d consider &#39;web quality&#39; compared to JPEG XL (particularly when <a href="/2021/serving-sharp-images-to-high-density-screens/">optimising for high-density</a>). And based on current implementations, AVIF decodes significantly quicker than JPEG XL. Smaller files and faster decode is a huge advantage, but that said, I&#39;d like AVIF to have a better &#39;progressive&#39; feature. It&#39;d be great if it supported an early render that allows more control over the quality and scale of the initial pass, and ideally a configurable post-process blur effect to hide compression artefacts.</p>
<p>Maybe the &#39;layers&#39; approach can be improved. Or maybe it&#39;s an over-complication. For example:</p>
<figure class="full-figure max-figure">
<div class="post-component-5"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">With blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Without blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">Final image</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-BhXDY77h.js';import Component from '/c/ImageTabs-MrFnKNkF.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["With blur","/c/blur-H6B46gPz.avif"],["Without blur","/c/fox-low-ZQiIxumG.avif"],["Final image","/c/fox-DSbxk4jA.avif"]]}), document.querySelector('.post-component-5'));</script>
</figure>

<p>The above image is 1.97 kB. It&#39;s scaled down, compressed with AVIF at minimum quality, and has a post-process blur applied.</p>
<p>With the blur applied, I think this works great as a preview. So could this… just go at the start of the full image file?</p>
<p>This means the &#39;preview&#39; is 100% overhead in terms of file size, but it also means as web developers, we can freely configure the quality of this &#39;preview&#39; by deciding how much overhead we find acceptable. Maybe I&#39;m being too conservative by only spending 1.97 kB, rather than, say, <span style="white-space: nowrap">10 kB</span>:</p>
<figure class="full-figure max-figure">
<div class="post-component-6"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">With blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Without blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">Final image</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-BhXDY77h.js';import Component from '/c/ImageTabs-MrFnKNkF.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["With blur","/c/less-blur-Cpj0-TVK.avif"],["Without blur","/c/fox-lowish-BiwxtDNq.avif"],["Final image","/c/fox-DSbxk4jA.avif"]]}), document.querySelector('.post-component-6'));</script>
</figure>

<p>And because it&#39;s an entirely separate image, the decoding overhead is zero if the browser doesn&#39;t see a benefit to displaying the preview. As in, in cases where the browser has the whole file, it can just skip the preview.</p>
<p>The post-process blur would need to be part of the format, rather than left to something like CSS. This would allow the format to use a &#39;cheap&#39; blur filter, avoiding expensive rendering costs.</p>
<p><a href="https://github.com/AOMediaCodec/av1-avif/issues/102">I originally pitched this idea back in 2020</a> before the &#39;layers&#39; idea was explored. But maybe it&#39;s time to consider it again. Here&#39;s hoping!</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fetch streams are great, but not for measuring upload/download progress]]></title>
        <id>https://jakearchibald.com/2025/fetch-streams-not-for-progress/</id>
        <link href="https://jakearchibald.com/2025/fetch-streams-not-for-progress/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-BGVsUjKW.png" type="image/png"/>
        <updated>2025-09-15T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[They&#39;re inaccurate, and there are better ways.]]></summary>
        <content type="html"><![CDATA[<p>Part of my role at Mozilla is making sure we&#39;re focusing on the right features, and we got onto the topic of fetch upload streams. It&#39;s something Chrome has supported for a while, but it isn&#39;t yet supported in either Firefox or Safari.</p>
<p>I asked folks on <a href="https://bsky.app/profile/jakearchibald.com/post/3lxws4wvgns27">various</a> <a href="https://mastodon.social/@jaffathecake/115140724791632506">social</a> <a href="https://x.com/jaffathecake/status/1963240536891895973">platforms</a> what they thought of the feature, and what they&#39;d use it for. The most common answer by far was &quot;to measure upload progress&quot;, but… using it for that will give inaccurate results, and may even lead to bad implementations in browsers.</p>
<p>Let&#39;s dig in…</p>
<h2 id="response-streams"><a href="#response-streams">Response streams</a></h2>
<p>Streaming responses have been available in all browsers for years now:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> reader <span class="token operator">=</span> response<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">getReader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> done<span class="token punctuation">,</span> value <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> reader<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>done<span class="token punctuation">)</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Done!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This lets you get the response chunk-by-chunk, so you can start processing it as you receive it.</p>
<p>This is even easier with async iterators:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> chunk <span class="token keyword">of</span> response<span class="token punctuation">.</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>chunk<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Done!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>…but that isn&#39;t supported in Safari. I&#39;ve <a href="https://github.com/web-platform-tests/interop/issues/1068">proposed it for interop 2026</a>.</p>
<p>The chunks are <code>Uint8Array</code>s, but you can <a href="https://developer.mozilla.org/docs/Web/API/TextDecoderStream/TextDecoderStream">use <code>TextDecoderStream</code></a> to get the chunks as text.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> textStream <span class="token operator">=</span> response<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">pipeThrough</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">TextDecoderStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><h3 id="but-theyre-not-ideal-for-measuring-download-progress"><a href="#but-theyre-not-ideal-for-measuring-download-progress">But they&#39;re not ideal for measuring download progress</a></h3>
<p>You could try to measure download progress like this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// THIS DOESN'T ALWAYS WORK!</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> contentLength <span class="token operator">=</span> <span class="token function">Number</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'Content-Length'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> downloaded <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>

<span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> chunk <span class="token keyword">of</span> response<span class="token punctuation">.</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  downloaded <span class="token operator">+=</span> chunk<span class="token punctuation">.</span>length<span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>contentLength<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>downloaded <span class="token operator">/</span> contentLength<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>The good part is that you&#39;re measuring the point at which you have the data, which is what matters when it comes to download. Let&#39;s say you were receiving three packages via mail – you&#39;re measuring the point each package arrives in your possession, which is fine.</p>
<p>However, this all falls down if the response has a <code>Content-Encoding</code> (such as brotli or gzip), because in that case the <code>Content-Length</code> represents the encoded size, but the chunks are <em>decoded</em> chunks. This means your <code>downloaded</code> value is likely to exceed the <code>contentLength</code> value.</p>
<p>Although, if you made that mistake, you wouldn&#39;t be alone. A few years ago <a href="https://x.com/jaffathecake/status/996720153575546880">I investigated how browsers handled download progress of compressed resources</a>, and the results were… messy.</p>
<p>There&#39;s <a href="https://github.com/whatwg/fetch/issues/1524">a feature request for a way to get the raw body</a>, without decompressing.</p>
<h2 id="request-streams"><a href="#request-streams">Request streams</a></h2>
<p>Request streams are basically the same as response streams, but for uploads rather than downloads. Imagine a video uploading app that did some transcoding/processing of the video before uploading:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get a video from disk, or from the camera</span>
<span class="token keyword">const</span> videoStream <span class="token operator">=</span> <span class="token function">getVideoStreamSomehow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Process it in some way, e.g. editing or transcoding</span>
<span class="token keyword">const</span> processedVideo <span class="token operator">=</span> videoStream<span class="token punctuation">.</span><span class="token function">pipeThrough</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">SomeVideoProcessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Upload the stream</span>
<span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span>
  <span class="token literal-property property">body</span><span class="token operator">:</span> processedVideo<span class="token punctuation">,</span>
  <span class="token literal-property property">duplex</span><span class="token operator">:</span> <span class="token string">'half'</span><span class="token punctuation">,</span>
  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'video/mp4'</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>In this model, the video processing can happen in parallel with the uploading. More traditional methods would require processing the whole video before the upload begins, which is potentially much slower.</p>
<h3 id="restrictions"><a href="#restrictions">Restrictions</a></h3>
<p><code>duplex: &#39;half&#39;</code> means that the request must complete before the response becomes available. This &#39;reserves&#39; a future feature where browsers can support handling the request and response in parallel.</p>
<p>If you want to have a request and response in parallel, you can perform two fetches – one that streams request data, and another that streams response data.</p>
<p>Also, since the <code>Content-Length</code> of the request isn&#39;t known, the feature is restricted to transports that are already used to handling request data in chunks, which is HTTP/2 onwards.</p>
<h3 id="but-theyre-bad-for-measuring-upload-progress"><a href="#but-theyre-bad-for-measuring-upload-progress">But they&#39;re bad for measuring upload progress</a></h3>
<p>I&#39;m not even going to show the code for this because it&#39;s too unreliable.</p>
<p>With <em>request</em> streams, you end up measuring the point at which the data is taken from your stream. In terms of the postal metaphor, you&#39;re measuring the point each package is collected by the courier, which does not represent the delivery of those packages.</p>
<p>Fetch is the courier. It will take some chunks off your hands and start handling the network parts. Successfully sending those chunks will happen later, or maybe it will fail.</p>
<p>This is normal behaviour with streams, and it helps keep everything running as smoothly as possible in cases where parts of the pipe flow quicker than others. Each stream has a &quot;high water mark&quot;, which is essentially an ideal amount to buffer.</p>
<h3 id="using-streams-to-measure-upload-progress-could-cause-problems-in-future"><a href="#using-streams-to-measure-upload-progress-could-cause-problems-in-future">Using streams to measure upload progress could cause problems in future</a></h3>
<p>If you try to use streams to measure upload progress, the results will be inaccurate, but it may also limit the quality of this feature in future.</p>
<p>Let&#39;s say browsers ship this, and do a fairly small amount of buffering. If folks use this for upload progress, it won&#39;t be right, but it might be close enough.</p>
<p>Then let&#39;s say a browser discovers that they can get better performance by buffering more within fetch. If large parts of the web are using this feature for upload progress, that browser faces two choices: Ship the change, which means these websites&#39; upload progress measurements become even less accurate, or avoid shipping the general improvement. I don&#39;t want browsers to end up having to make a compromise here.</p>
<h2 id="how-to-measure-upload-progress"><a href="#how-to-measure-upload-progress">How to measure upload progress</a></h2>
<p>Upload and download progress is unfortunately a missing feature in fetch. If you want progress events <em>today</em>, the best way is to use XHR:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> xhr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">XMLHttpRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Upload progress</span>
xhr<span class="token punctuation">.</span>upload<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Uploaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

<span class="token comment">// Download progress</span>
xhr<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

xhr<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token string">'POST'</span><span class="token punctuation">,</span> url<span class="token punctuation">)</span><span class="token punctuation">;</span>
xhr<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>blobOrWhatever<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><h2 id="but-in-future"><a href="#but-in-future">But in future…</a></h2>
<p><a href="https://bsky.app/profile/lukewarlow.dev">Luke Warlow from Igalia</a> is currently working on <a href="https://github.com/whatwg/fetch/pull/1843">an API for fetch</a> to add progress events for both uploads and downloads.</p>
<p>The API might change before it lands, but right now it looks like this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token function">observer</span><span class="token punctuation">(</span><span class="token parameter">requestObserver<span class="token punctuation">,</span> responseObserver</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    requestObserver<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
          <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Uploaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    responseObserver<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
          <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This should give us all the modern benefits of <code>fetch</code>, while plugging the progress feature gap!</p>
<p>If you&#39;re interested in using request streams for things other than progress events, there&#39;s <a href="https://github.com/web-platform-tests/interop/issues/1072">an Interop 2026 proposal for them</a>. We also want to gauge developer interest <a href="https://survey.alchemer.com/s3/8460326/Fetch-Request-Streaming">via this survey</a>, or via the interop GitHub thread.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Making XML human-readable without XSLT]]></title>
        <id>https://jakearchibald.com/2025/making-xml-human-readable-without-xslt/</id>
        <link href="https://jakearchibald.com/2025/making-xml-human-readable-without-xslt/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-09-02T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[JavaScript is right there.]]></summary>
        <content type="html"><![CDATA[<p>Ok. This one is niche. But heh, you might enjoy a dive into this esoteric corner of the web – I certainly did.</p>
<h2 id="xslt-and-browser-support"><a href="#xslt-and-browser-support">XSLT and browser support</a></h2>
<p>I want to get to the technical-fun bits quickly, so here&#39;s a quick rundown:</p>
<ul>
<li>XSLT is an XML language for transforming XML, including transforming it into non-XML formats, such as HTML.</li>
<li>Browsers currently support a ~25 year old version of XSLT natively (examples coming up later).</li>
<li>This feature is barely used. Usage is lower than features that were subsequently removed from browsers, such as mutation events and Flash.</li>
<li>The feature is often the source of <a href="https://www.neowin.net/news/google-project-zero-exposes-security-flaw-in-libxslt-library-used-in-gnome-applications/">significant security issues</a>.</li>
</ul>
<p>This leaves browsers with a choice: Take development time away from features that have significant usage and developer demand, and instead use those resources to improve (probably rewrite) XSLT processing in browsers. Or, remove XSLT from browser engines.</p>
<p>Currently, <a href="https://github.com/whatwg/html/issues/11523#issue-3285251497">Chrome</a>, <a href="https://github.com/whatwg/html/issues/11523#issuecomment-3149280766">Safari</a>, and <a href="https://github.com/mozilla/standards-positions/issues/1287#issuecomment-3227145793">Firefox</a> support removing XSLT.</p>
<p>If you want to know more of the process &amp; history here, I recommend <a href="https://meyerweb.com/eric/thoughts/2025/08/22/no-google-did-not-unilaterally-decide-to-kill-xslt/">Eric Meyer&#39;s blog post</a>.</p>
<p>Right, let&#39;s take a look at XSLT and the alternatives.</p>
<h2 id="how-xslt-works"><a href="#how-xslt-works">How XSLT works</a></h2>
<p>Take some XML like this:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml version="1.0" encoding="UTF-8"?></span>
<span class="token prolog">&lt;?xml-stylesheet type="text/xsl" href="books.xsl"?></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>authors</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>author</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>name</span><span class="token punctuation">></span></span>Douglas Adams<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>name</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>The Hitchhiker's Guide to the Galaxy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>genre</span><span class="token punctuation">></span></span>Science Fiction Comedy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>genre</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>cover</span><span class="token punctuation">></span></span>https://covers.openlibrary.org/b/isbn/0330258648-L.jpg<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>cover</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      …
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
    …
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>author</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>author</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>name</span><span class="token punctuation">></span></span>George Orwell<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>name</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      …
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>author</span><span class="token punctuation">></span></span>
  …
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>authors</span><span class="token punctuation">></span></span></code></pre></div><p>If you <a href="/demos/xml-transformation/via-xslt/index.xml">visit that document</a>, you&#39;ll (for now, at least), see a grid of books, with formatting and images, and that&#39;s all down to:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml-stylesheet type="text/xsl" href="books.xsl"?></span></code></pre></div><p>…which tells the browser to transform the XML using XSLT.</p>
<p><a href="https://github.com/jakearchibald/jakearchibald.com/blob/main/root-static/demos/xml-transformation/via-xslt/books.xsl">Here&#39;s the full source</a> if you&#39;re interested, but here&#39;s the snippet that renders each book:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token namespace">xsl:</span>template</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>book<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>book<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cover<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{cover}<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>book-info<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span>
        -
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>author<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>../name<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>genre<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>genre<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token namespace">xsl:</span>template</span><span class="token punctuation">></span></span></code></pre></div><p>This template maps a <code>&lt;book&gt;</code> into a <code>&lt;div class=&quot;book&quot;&gt;</code>, although it reaches up into the parent <code>&lt;author&gt;</code> to get the author name.</p>
<p>I used to write XSLT as part of my first job ~20 years ago. Making this demo felt <em>deeply weird</em>.</p>
<p>Anyway, assuming the above will stop working someday, what are the alternatives?</p>
<h2 id="styling-xml"><a href="#styling-xml">Styling XML</a></h2>
<p>In some cases, you might get away with just styling XML, which you can do using:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml-stylesheet type="text/css" href="styles.css"?></span></code></pre></div><p>Now you can style your XML document using regular CSS. Just make sure your document is served with <code>Content-Type: text/xml</code> (rather than, e.g. a more specific RSS content type), otherwise the browser won&#39;t use the styles.</p>
<p>What you can do here is pretty limited. In the XSLT example, I take the text content of the <code>&lt;cover&gt;</code> element, and make it the <code>src</code> of an image. I also repeat the author&#39;s <code>&lt;name&gt;</code> for each book. Neither of these is possible with CSS alone.</p>
<p>Also, the document will have the semantics of the XML document, which likely means &quot;no semantics&quot;, so the result will have poor accessibility. For example, even if your XML contains <code>&lt;h1&gt;</code>, unless you&#39;ve told it to be an HTML element, the browser won&#39;t recognise it as a heading.</p>
<p>That said, if you just want to catch users that mistakenly land on some XML, you can get away with something like this:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">:root</span> <span class="token punctuation">{</span>
  <span class="token selector">&amp; > *</span> <span class="token punctuation">{</span>
    <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token selector">&amp;::before</span> <span class="token punctuation">{</span>
    <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'Hi there! This is my RSS feed. '</span>
      <span class="token string">"It isn't meant to be human-readable…"</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Which is what I&#39;ve done with <a href="/posts.rss">my RSS feed</a>.</p>
<p>For anything more complex, you need to do some kind of transformation of the source data.</p>
<h2 id="wait-do-you-really-want-to-do-this-client-side"><a href="#wait-do-you-really-want-to-do-this-client-side">Wait, do you really want to do this client-side?</a></h2>
<p>In almost all cases, the best thing to do is convert your data to HTML on the server, or as part of a build process. When you do that:</p>
<ul>
<li>The HTML form of your data is more likely to be seen by crawlers and scrapers.</li>
<li>The browser can render the page in a streaming way, meaning a faster experience for users.</li>
<li>You can use whatever tools you like to do the transformation – you aren&#39;t limited to things the browser supports natively.</li>
</ul>
<p>This might mean you end up with two published files, one for humans (as HTML), and one for machines (as XML or whatever). This is fine. My blog posts and my RSS feed are different resources, even though they have data in common. All my posts have this in the head:</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span>
  <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>alternate<span class="token punctuation">"</span></span>
  <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>application/rss+xml<span class="token punctuation">"</span></span>
  <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Jake Archibald's Blog<span class="token punctuation">"</span></span>
  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://jakearchibald.com/posts.rss<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span></code></pre></div><p>…meaning feed readers can find the RSS feed from any post.</p>
<p>But if you really need to, here&#39;s how to do the transformation on the client, without XSLT:</p>
<h2 id="transforming-xml-client-side-without-xslt"><a href="#transforming-xml-client-side-without-xslt">Transforming XML client-side without XSLT</a></h2>
<p>If you just want to make some existing XSLT work in browsers that don&#39;t support it, check out <a href="https://github.com/mfreed7/xslt_polyfill">Mason&#39;s XSLT polyfill</a>. But if you don&#39;t need to use XSLT, JavaScript is, in my opinion, a better option.</p>
<p><a href="/demos/xml-transformation/via-js/index.xml">Here&#39;s a demo</a> that produces the same result as the XSLT demo, but this time using JS. Here&#39;s how it works…</p>
<p>In the XML, instead of including XSLT, I include a script as the first child of the root:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>authors</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span>
    <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/1999/xhtml<span class="token punctuation">"</span></span>
    <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>script.js<span class="token punctuation">"</span></span>
    <span class="token attr-name">defer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span>
  <span class="token punctuation">/></span></span>
  …</code></pre></div><p>The <code>xmlns</code> attribute is important, as it tells the browser this is an HTML script element, rather than some other kind of XML element.</p>
<p>Technically, this is changing the data structure. Validating parsers would see a <code>&lt;script&gt;</code> they weren&#39;t expecting, and throw an error. However, most parsers are not validating, as they want to allow for extensions to whatever format they&#39;re reading.</p>
<p>Then, in that script:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get the XML data</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">;</span>
<span class="token comment">// Create an HTML root element</span>
<span class="token keyword">const</span> htmlEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElementNS</span><span class="token punctuation">(</span>
  <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">,</span>
  <span class="token string">'html'</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Swap the two over</span>
data<span class="token punctuation">.</span><span class="token function">replaceWith</span><span class="token punctuation">(</span>htmlEl<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Now all I had to do was populate that <code>html</code> element. I created <a href="https://github.com/jakearchibald/jakearchibald.com/blob/main/root-static/demos/xml-transformation/via-js/html.js">a simple templating function</a> to handle escaping. For example, here&#39;s the snippet that renders each book:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token function-variable function">bookHTML</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">bookEl</span><span class="token punctuation">)</span> <span class="token operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
  &lt;div class="book">
    &lt;img
      class="cover"
      src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > cover'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"
      alt=""
    />
    &lt;div class="book-info">
      &lt;div>
        &lt;span class="title">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > title'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
        -
        &lt;span class="author">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > name'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
      &lt;/div>
      &lt;div>
        &lt;span class="genre">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > genre'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
      &lt;/div>
    &lt;/div>
  &lt;/div>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre></div><p>It&#39;s very similar to the XSLT equivalent, <em>and</em> it can be debugged using regular JavaScript tooling.</p>
<h3 id="ensure-youre-creating-_html_-elements"><a href="#ensure-youre-creating-_html_-elements">Ensure you&#39;re creating <em>HTML</em> elements</a></h3>
<p>One gotcha when creating HTML in an XML document is it&#39;s easy to accidentally create XML elements rather than HTML elements. If you want proper semantics, accessibility, and behaviour, you want real HTML elements.</p>
<p>For example:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// In an XML document this will not create an HTML element:</span>
<span class="token keyword">const</span> xmlElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Instead, you need to use:</span>
<span class="token keyword">const</span> htmlElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElementNS</span><span class="token punctuation">(</span>
  <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">,</span>
  <span class="token string">'h1'</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>If you&#39;re using a framework to create the elements, check that it&#39;s creating real HTML elements – <code>el.namespaceURI</code> should be <code>&quot;http://www.w3.org/1999/xhtml&quot;</code>.</p>
<p>The <code>xmlns</code> attribute we used earlier only works via the parser, so this <em>doesn&#39;t</em> create an HTML element in an XML document:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> h1 <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
h1<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'xmlns'</span><span class="token punctuation">,</span> <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>However, <code>innerHTML</code> and <code>setHTMLUnsafe</code> will assume the content is HTML, which is what I used:</p>
<div class="code-example"><pre class="language-js"><code>htmlEl<span class="token punctuation">.</span><span class="token function">setHTMLUnsafe</span><span class="token punctuation">(</span>html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
  &lt;head>
    &lt;title>Some books&lt;/title>
    &lt;meta name="viewport" content="width=device-width, initial-scale=1" />
  &lt;/head>
  &lt;body>
    &lt;h1>Some books&lt;/h1>
    &lt;ul class="book-list">
      </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">[</span><span class="token operator">...</span>data<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'book'</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>
        <span class="token punctuation">(</span><span class="token parameter">book</span><span class="token punctuation">)</span> <span class="token operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">bookHTML</span><span class="token punctuation">(</span>book<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/li></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
    &lt;/ul>
  &lt;/body>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Here&#39;s <a href="/demos/xml-transformation/via-js/index.xml">the full working demo</a>, and <a href="https://github.com/jakearchibald/jakearchibald.com/tree/main/root-static/demos/xml-transformation/via-js">the source</a>. And that&#39;s it! Wow you made it to the end. Huh. I didn&#39;t think anyone would.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Give footnotes the boot]]></title>
        <id>https://jakearchibald.com/2025/give-footnotes-the-boot/</id>
        <link href="https://jakearchibald.com/2025/give-footnotes-the-boot/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-07-01T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[I hate footnotes, and hopefully by the end of this, you will too.]]></summary>
        <content type="html"><![CDATA[<style>
  html {
    scroll-padding-top: 1.3rem;
  }
  @keyframes ref-alert {
    0% {
      transform: scale(17);
      opacity: 0;
    }
    20% {
      opacity: 1;
    }
    80% {
      opacity: 1;
    }
    100% {
      transform: scale(1);
      opacity: 0;
    }
  }
  sup.ref {
    margin-left: 0.09em;
    display: inline-block;
    position: relative;
  }
  .target-effect:target::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 1.2em;
    aspect-ratio: 1 / 1;
    border-radius: 1000px;
    border: 3px solid red;
    translate: -50% -50%;
    opacity: 0;
    animation: ref-alert 0.8s ease;
  }
  .footnotes {
    font-size: 0.85em;
    > ol {
      padding-inline-start: 0;
      list-style-position: inside;
      > li {
        margin-bottom: 1em;
      }
    }
  }
  .footnote {
    background-image: linear-gradient(to right, transparent 0%, yellow 0%, yellow 100%, transparent 100%);
    background-position: left top;
    background-repeat: no-repeat;
    transition: background-size 1.7s ease;
    background-size: 0% 100%;
    &:target {
      @starting-style {
        background-size: 0% 100%;
      }
      background-size: 100% 100%;
    }
  }
  a:active {
    outline: 2px solid red;
  }
  .footnote-marker-button {
    all: unset;
    position: inline-block;
    cursor: pointer;
    color: #cc2100;
    &:hover, &:focus {
      color: #ff8f00;
    }
  }
  @keyframes backdrop-in {
    from {
      opacity: 0;
    }
  }
  @keyframes popover-in {
    from {
      opacity: 0;
      translate: 0 20px;
    }
  }
  .footnote-popover {
    &:not(:popover-open) {
      all: unset;
    }

    --side-margin: 20px;
    --border-radius: 7px;

    @media (width >= 530px) {
      --side-margin: 30px;
    }

    &:popover-open {
      all: unset;
      font-size: 1rem;

      .footnote-popover-content {
        position: fixed;
        background: #fff;
        padding: 0.9em 1.1em;
        max-width: min(476px, calc(100vw - 60px));
        border-radius: var(--border-radius);
        animation: popover-in 0.2s ease;
      }

      &::backdrop {
        background: rgba(0, 0, 0, 0.3);
        animation: backdrop-in 0.2s ease;
      }
    }

    @supports not (bottom: anchor(top)) {
      .footnote-popover-content {
        margin: auto;
        inset: 0;
        height: fit-content;
      }
    }

    @supports (bottom: anchor(top)) {
      &:popover-open {
        --arrow-size: 10px;

        .footnote-popover-content {
          position-anchor: var(--position-anchor);
          bottom: anchor(top);
          justify-self: anchor-center;
          margin: 0 var(--side-margin) var(--arrow-size);
          max-width: 476px;
        }

        &::after {
          position-anchor: var(--position-anchor);
          content: '';
          position: fixed;
          bottom: anchor(top);
          justify-self: anchor-center;
          border: calc(var(--arrow-size) + 1px) solid transparent;
          border-bottom-width: 0;
          border-top-color: #fff;
          width: 0;
          height: 0;
          margin: 0 calc(var(--side-margin) + var(--border-radius));
          animation: popover-in 0.2s ease;
        }
      }
    }
  }
  [role=note] {
    margin: 0 -20px;
    padding: 0 20px;
    background: #dbf3f7;
    display: flow-root;

    @media (width >= 530px) {
      padding: 0 1.6em;
      margin: 0;
    }

    > h4:first-child {
      margin-top: 1.3em;
    }
  }
  :root {
    interpolate-size: allow-keywords;
  }
  .details-demo {
    margin: 0 -20px;
    background: #dbf3f7;
    display: flow-root;
    overflow: clip;

    @media (width >= 530px) {
      margin: 0;
    }

    summary {
      margin: 1.2em 0;
      padding: 0 20px;

      @media (width >= 530px) {
        padding: 0 30px;
      }
    }

    .details-content {
      padding: 0 20px;

      @media (width >= 530px) {
        padding: 0 30px;
      }

      > p:first-child {
        margin-top: 0;
      }
    }

    .code-example {
      margin-bottom: 0;
      --unmargin: 20px;
      margin-left: calc(var(--unmargin) * -1);
      margin-right: calc(var(--unmargin) * -1);
      padding-left: var(--unmargin);
      padding-right: var(--unmargin);

      @media screen and (min-width: 530px) {
        --unmargin: 32px;
      }
    }

    &::details-content {
      height: 0;
      overflow: clip;
      display: flow-root;
      transition: height 0.5s ease, content-visibility 0.5s ease allow-discrete;
    }
    &[open]::details-content {
      height: auto;
    }
  }
</style>

<p>I hate footnotes<sup class="ref">1</sup>, and hopefully by the end of this, you will too. Let&#39;s get down to it…</p>
<h2 id="the-ux-of-footnotes-in-printed-media"><a href="#the-ux-of-footnotes-in-printed-media">The UX of footnotes in printed media</a></h2>
<p>You, the reader, encounter a tiny number<sup class="ref">2</sup> within some prose. This indicates to you that I, the writer, have something more to say on this topic. And, for your inconvenience, I&#39;ve put it way down at the bottom of the page.</p>
<p>The choice is yours: do you skip over it, and stay in the flow of the article, or do you set off on a side-quest to discover the extra wisdom I have to offer?</p>
<p>It&#39;s like one of those &quot;choose your own adventure&quot; books, except you know the destination is a dead-end, meaning you&#39;ll have to re-trace your steps so you can continue the main thread of the article.</p>
<p>Maybe if you know me well enough, you can be in tune with the kinds of things that I&#39;d choose to leave in a footnote, and you can make some kind of judgement on whether it&#39;s likely to be worth your time. But since this is the first time I&#39;ve used footnotes in an article<sup class="ref">3</sup>, you don&#39;t really have anything to go on. All that little 3 tells you is that there&#39;s some additional content that may or may not be contextually useful to you, and that it&#39;s the third time I&#39;ve done this to you.</p>
<p>Whether you go out of your way to read the footnote content is really just a test of your curiosity.</p>
<p>If you did make your way down to the footnotes, I hope you appreciated that I set the text size to be a little smaller than the &#39;ideal&#39; size that I chose for the main body content, making them harder to read, such is the tradition within the footnote community.</p>
<p>And if you enjoyed all of that, I have <em>great news for you</em>: many people bring this experience to the world wide web.</p>
<h2 id="footnotes-on-the-web"><a href="#footnotes-on-the-web">Footnotes on the web</a></h2>
<p>If reproduced as-is, footnotes on the web are even worse than their printed counterparts. In print, the footnotes are usually at the bottom of the current page, so they&#39;re a quick glance away, and you can use a finger to mark your place in the main text while you&#39;re off on your side-quest. Whereas on this beautiful pageless web of ours, you have to scroll all the way down to the end of the article. You can try flinging right to the bottom of the document, but if there&#39;s a sizeable footer or comments section, you&#39;ll overshoot the footnotes. And of course, after all of this, you have to scroll back to where you were, which is easier said than done.</p>
<p>However, most webby footnoters have realised that in the printed world, the numbering of footnotes is used to manually <em>link</em> the primary content to the supplemental content, and the web has a dedicated feature to enhance connections like this: hyperlinks<a href="#footnote-4"><sup class="ref">4</sup></a>.</p>
<p>Of course, with footnotes being at the bottom of the article, sometimes these links will simply take you to the end of the document, so you&#39;ll <em>still</em> have to visually scan for the pertinent footnote<a href="#footnote-5"><sup class="ref">5</sup></a>.</p>
<p>If the footnote markers are links, then the user can use the back button/gesture to return to the main content. But, even though this restores the previous scroll position, the user is still left with the challenge of finding their previous place in a wall of text<a href="#footnote-6"><sup class="ref target-effect" id="footnote-marker-6">6</sup></a>.</p>
<p>We could try to solve that problem by dynamically pulling the content from the footnotes and displaying it in a popover. In some browsers <span class="popover-support-commentary"></span> that will display like a tooltip, pointing directly back to the footnote marker. Thanks to modern web features, this can be done entirely without JavaScript<button class="footnote-marker-button" popovertarget="footnote-7" style="anchor-name: --footnote-7"><sup class="ref">7</sup></button>.</p>
<script>
  {
    const el = document.querySelector('.popover-support-commentary');
    if (CSS.supports('bottom: anchor(top)')) {
      el.textContent = '(including yours)';
    } else {
      el.textContent = '(not yours, sorry)';
    }
  }
</script>

<p>But this is still shit! I see good, smart people, who&#39;d always <a href="https://www.w3.org/QA/Tips/noClickHere">avoid using &quot;click here&quot; as link text</a>, littering their articles with link texts such as <sup>1</sup>, <sup>7</sup>, and <em>sometimes even <sup>12</sup></em>. Not only is this as contextless as &quot;click here&quot;, it also provides the extra frustration of a tiny-weeny hit target.</p>
<p><em>Update:</em> Adrian Roselli pointed out that there are <a href="https://adrianroselli.com/2022/09/brief-note-on-super-and-subscript-text.html">numerous bugs with accessibility tooling and superscript</a>.</p>
<p>And all this for what? To cargo-cult academia? Stop it! Stop it now! Footnotes are a shitty hack built on the limitations of printed media. It&#39;s dumb to build on top of those limitations when they don&#39;t exist on the web platform. So I ask you to break free of footnotes and do something better.</p>
<h2 id="alternatives-to-footnotes-on-the-web"><a href="#alternatives-to-footnotes-on-the-web">Alternatives to footnotes on the web</a></h2>
<p>Here are the goals:</p>
<ul>
<li>Provide easily-read supplementary content.</li>
<li>Let the user easily skip over it if they&#39;re not interested.</li>
<li>Make it easy for the user to decide if they&#39;re interested or not.</li>
</ul>
<p>Given that:</p>
<h3 id="just-like-parentheses"><a href="#just-like-parentheses">Just, like… parentheses?</a></h3>
<p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy (unless you&#39;re nesting them – don&#39;t do that), and it keeps the reader in the flow of the article.</p>
<h3 id="a-note-section"><a href="#a-note-section">A &#39;note&#39; section</a></h3>
<p>If your content is a little longer, you can use <code>&lt;section role=&quot;note&quot;&gt;</code> and style it to look self-contained.</p>
<section role="note">

<h4 id="why-not-aside"><a href="#why-not-aside">Why not <code>&lt;aside&gt;</code>?</a></h4>
<p>I thought <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/aside">the <code>&lt;aside&gt;</code> element</a> would be ideal for this, but that uses <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/complementary_role">the <code>complementary</code> role</a> which doesn&#39;t seem like a good fit for this kind of content. Whereas <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/note_role">the <code>note</code> role</a> is specifically for &quot;parenthetic or ancillary content&quot;, which sounds perfect.</p>
</section>

<p>The heading makes the topic of the note clear, and since the content is semantically and visually contained, it&#39;s easy to skip over.</p>
<h3 id="a-details-section"><a href="#a-details-section">A &#39;details&#39; section</a></h3>
<p>If your content is too long for a note, consider using <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details"><code>&lt;details&gt;</code> and <code>&lt;summary&gt;</code></a>.</p>
<details class="details-demo">
  <summary>Animating the height of the <code>&lt;details&gt;</code> element</summary>

  <div class="details-content">

<p>Thanks to a few newer CSS features (currently only available in Chromium), such as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::details-content">the <code>::details-content</code> pseudo-element</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior#allow-discrete"><code>allow-discrete</code> transitions</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size">interpolate-size</a>, we can animate the opening and closing of <code>&lt;details&gt;</code> elements.</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">:root</span> <span class="token punctuation">{</span>
  <span class="token property">interpolate-size</span><span class="token punctuation">:</span> allow-keywords<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">.details-demo</span> <span class="token punctuation">{</span>
  <span class="token selector">&amp;::details-content</span> <span class="token punctuation">{</span>
    <span class="token property">height</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
    <span class="token property">overflow</span><span class="token punctuation">:</span> clip<span class="token punctuation">;</span>
    <span class="token property">display</span><span class="token punctuation">:</span> flow-root<span class="token punctuation">;</span>
    <span class="token property">transition</span><span class="token punctuation">:</span>
      height 0.5s ease<span class="token punctuation">,</span>
      content-visibility 0.5s ease allow-discrete<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token selector">&amp;[open]::details-content</span> <span class="token punctuation">{</span>
    <span class="token property">height</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div>  </div>
</details>

<p>Like the note section, the summary makes the topic of the details clear, and since the content is semantically and visually contained, it&#39;s easy to skip over. The benefit with <code>&lt;details&gt;</code> is the user doesn&#39;t have to scroll past lots of content if they&#39;re not interested.</p>
<p>I&#39;m not a UX designer, so maybe you can think of better patterns. But if the alternative is footnotes, the bar is lowwwww.</p>
<h2 id="update-are-my-alternatives-too-disruptive"><a href="#update-are-my-alternatives-too-disruptive">Update: are my alternatives too disruptive?</a></h2>
<p>I&#39;ve really enjoyed the reasoned feedback to this post. Particularly those from <a href="https://front-end.social/@leaverou/114784950642671149">Lea Verou</a>, and <a href="https://www.kryogenix.org/days/2025/07/03/a-limited-defence-of-footnotes/">Stuart Langridge</a>. I recommend reading them in full (and also try a &#39;chello-bull for yourself), but I want to respond to the general sentiment that the alternatives are <em>too disruptive</em>.</p>
<p>Let&#39;s take my parenthetical example:</p>
<blockquote class="quote">

<p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy (unless you&#39;re nesting them – don&#39;t do that), and it keeps the reader in the flow of the article.</p>
</blockquote>

<p>Where the user experience is:</p>
<ol>
<li>Read &quot;scanning for the next closing parenthesis is pretty easy&quot;.</li>
<li>Read &quot;(unless you&#39;re nesting them&quot;.</li>
<li>Then one of the following:<ul>
<li>Read &quot; – don&#39;t do that)&quot;.</li>
<li>Or, if you&#39;ve become disinterested in this supplementary information, scan along to &quot;)&quot;.</li>
</ul>
</li>
<li>Continue reading.</li>
</ol>
<p>And compare it to the popover-footnote experience:</p>
<blockquote class="quote">

<p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy<button class="footnote-marker-button" popovertarget="footnote-8" style="anchor-name: --footnote-8"><sup class="ref">8</sup></button>, and it keeps the reader in the flow of the article.</p>
</blockquote>

<p>Where the user experience is:</p>
<ol>
<li>Read &quot;scanning for the next closing parenthesis is pretty easy<sup class="ref">8</sup>&quot;.</li>
<li>Decide, given no real clue as to the content of the footnote, to do one of the following:<ul>
<li>Explore, by clicking the <sup>8</sup>:<ol>
<li>Read &quot;Unless you&#39;re nesting them – don&#39;t do that.&quot;</li>
<li>Click outside the popover.</li>
<li>Continue reading.</li>
</ol>
</li>
<li>Or, skip over the <sup>8</sup>.</li>
</ul>
</li>
<li>Continue reading.</li>
</ol>
<p>Hopefully it&#39;s clear from the above that the simple parentheses solution is less disruptive, except maybe in the case where the reader is by-default disinterested in the extra content. And that&#39;s where the argument loses me.</p>
<p><strong>Why are you optimising the reading experience for people disinterested in your content?</strong> Like, if the content is really that pointless, don&#39;t put it in the article – that&#39;s even less disruptive! But, if you&#39;ve got something interesting to say, say it in context. If it&#39;s really optional, make it easy for readers to skip it once they realise it&#39;s not for them. Skipping over less-relevant content is a normal part of reading. We don&#39;t need to over-engineer it.</p>
<details class="details-demo">
  <summary>What about sidenotes?</summary>

  <div class="details-content">

<p>One of the footnote patterns I didn&#39;t explore were <a href="https://scottstuff.net/posts/2024/12/17/more-notes-on-notes/">sidenotes</a>, which only really work on desktop. But the disruption is similar. You see the superscript, and then you have to find the related sidenote (which is especially annoying in implementations that <code>Math.random()</code> the note to either the left or right), then visually find your way back to the main content.</p>
</details>

<p>All that said, I will admit I was really pleased with the no-JS popover footnotes when I got them working (although they&#39;re Chromium-only right now). But, I think that might be more down to the excitement of playing with a new CSS feature for the first time. Oh, on the topic of CSS anchors, my advice is to ignore <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position-area"><code>position-area</code></a>, which included too much magic for me to figure out, and instead use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/anchor"><code>anchor()</code></a>. Although your mileage may vary.</p>
<p>You&#39;ll be glad to hear my new job starts in August, so I won&#39;t have as much time to waste on posts like this.</p>
<aside class="footnotes">

<h2 id="footnotes-sigh"><a href="#footnotes-sigh">Footnotes (sigh)</a></h2>
<ol>
  <li>As always there are exceptions. Footnotes on Wikipedia avoid many of the pitfalls because they're only used for references, and they're a big enough site that visitors are frequent, and get used to that pattern. Although, I've only just noticed they use letter markers for 'notes' and numbers for 'references'. Also, I guess if your content is intended to be printed, then you are indeed tied to the limitations of printed media – although with media queries, you can tailor the experience specifically when printed. I don't have those requirements, so you won't catch me using footnotes again<sup class="ref">3</sup>.</li>
  <li>Not all footnotes are numbers, some are symbols, such as †.</li>
  <li style="list-style-type: '†. '">Oh, sorry, that wasn't an actual footnote marker.</li>
  <li value="3">Because, like I said, I hate them<sup class="ref">1</sup>.</li>
  <li><span id="footnote-4"></span>This is done using <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a">the anchor element</a>, which is "baseline widely available".</li>
  <li><span id="footnote-5" class="footnote"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:target">CSS's <code>:target</code> pseudo-class</a> can help here by highlighting the linked footnote.</span></li>
  <li><span id="footnote-6" class="footnote">Some footnote authors resolve this by adding a link back to the footnote marker, some also employ <code>:target</code> styles to help the user find their previous place in the document. <a href="#footnote-marker-6">⇐</a></span></li>
  <li><span id="footnote-7" class="footnote-popover" popover style="--position-anchor: --footnote-7"><span class="footnote-popover-content">This is done using a combination of <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API"><code>popover</code>, <code>popovertarget</code></a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning">CSS anchor positioning</a>. Elements with the <code>popover</code> attribute don't usually display until activated, but I've forced it to display using CSS, which is how this content also appears in the footnotes.</span></span></li>
  <li><span id="footnote-8" class="footnote-popover" popover style="--position-anchor: --footnote-8"><span class="footnote-popover-content">Unless you're nesting them – don't do that.</span></span></li>
</ol>

</aside>

<p>There. I hope you&#39;re happy.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Animating zooming using CSS: transform order is important… sometimes]]></title>
        <id>https://jakearchibald.com/2025/animating-zooming/</id>
        <link href="https://jakearchibald.com/2025/animating-zooming/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-BwRdLmfG.png" type="image/png"/>
        <updated>2025-06-17T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to get the right transform animation.]]></summary>
        <content type="html"><![CDATA[<p>I was using Discord the other day. I tapped to zoom into an image, and it animated in an odd way that I&#39;d seen before. Like this:</p>
<style>
  .full-figure img {
    height: auto;
  }
  .demo-buttons {
    display: flex;
    gap: 6px;
    flex-flow: row wrap;
  }
  .demo-container {
    overflow: clip;
    perspective: 1000px;
  }
  .scale-first-demo.zoom {
    transform: scale(3) translate(-33.1%, 20.2%);
  }
  .translate-first-calc-demo.zoom {
    --scale: 3;
    scale: var(--scale);
    translate: calc(-33.1% * var(--scale)) calc(20.2% * var(--scale));
  }
  .translate-z-demo.zoom {
    translate: -33.1% 20.2% 666.666px;
  }
  .zoomer {
    transition: transform 1s ease;
    transition-property: transform, translate, scale;
    position: relative;
    will-change: transform;
  }
  .zoomer-initial-rotate {
    transform: rotate(0);
  }
  .cat-sharp {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    transform: scale(0.3333333) translate(99.3%, -60.7%);
    will-change: transform;
  }
</style>

<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>Notice how it kinda &#39;swoops&#39; into the wildcat&#39;s face, rather than zooming straight in? See how the right-hand side of the cat&#39;s head goes out-of-frame, and then back in again?</p>
<p>I recognised it immediately because I&#39;d made the same mistake myself on another project.</p>
<p>The CSS is pretty simple:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>But watch this… If I change the starting transform from the default (<code>none</code>), to <code>rotate(0)</code>:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>The animation changes:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer zoomer-initial-rotate scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>Weird, huh? You wouldn&#39;t expect something like <code>rotate(0)</code>, which is equivalent to <code>none</code>, to completely change how the animation works, but this is happening entirely as designed in the CSS spec.</p>
<p>Let&#39;s dig into why.</p>
<h2 id="what-caused-the-quirky-animation"><a href="#what-caused-the-quirky-animation">What caused the quirky animation?</a></h2>
<p>Let&#39;s remove the <code>rotate(0)</code> for now and go back to the original code:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>When zooming into part of an element, <code>scale(n) translate(x, y)</code> feels like the easiest way to do it. You use the <code>translate</code> to get the subject into the centre, then adjust the <code>scale</code> to zoom in. Tweaking the values in DevTools is easy, as is calculating the values in code.</p>
<p>However, while this order of values is easy to write, it doesn&#39;t produce the most natural animation.</p>
<h3 id="how-transforms-are-animated"><a href="#how-transforms-are-animated">How transforms are animated</a></h3>
<p>The CSS spec has a <a href="https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms">somewhat complex algorithm</a> to decide how to animate transforms. For our values, it takes the <code>from</code> and <code>to</code> values:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>…and begins by padding them out, so they have the same number of components:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> none none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Then, for each <code>from</code> and <code>to</code> pair of components, it converts them to use a common function that can express both types of value. In this case:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Now that the transforms are in a similar format, it produces an animation that linearly interpolates each component separately. This means that the <code>scale</code> is animated linearly from <code>1</code> to <code>3</code>, and the <code>translate</code> is animated linearly from <code>0, 0</code> to <code>-33.1%, 20.2%</code>.</p>
<p>The animation itself isn&#39;t linear, as easing is applied, but linear interpolation is used as a starting point.</p>
<p>The problem is, with <code>scale</code> followed by <code>translate</code>, the <code>scale</code> acts as a multiplier for the <code>translate</code> values. Therefore, as the <code>scale</code> increases, even though the <code>translate</code> values are interpolated linearly, the effect is non-linear:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>At the start of the animation, a 1px shift in the <code>translate</code> value results in a ~1px shift on screen, as the <code>scale</code> is ~1. But, towards the end of the animation, a 1px shift in the <code>translate</code> value results in a ~3px shift on screen, as the <code>scale</code> is ~3. The position appears to change faster towards the end of the animation, which creates the &#39;swooping&#39; effect.</p>
<h2 id="how-to-fix-it"><a href="#how-to-fix-it">How to fix it</a></h2>
<p>To fix this, we need to avoid the <code>scale</code> acting as a multiplier for the <code>translate</code>, and the way to do this is to put the <code>translate</code> first.</p>
<p>We can&#39;t just swap the order, since we&#39;re relying on the multiplying effect of the <code>scale</code> to make the <code>translate</code> do the right thing. To achieve the same effect, we need to manually multiply the <code>translate</code> values by the <code>scale</code> value:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translate</span><span class="token punctuation">(</span>-99.3%<span class="token punctuation">,</span> 60.6%<span class="token punctuation">)</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>And that&#39;s it!</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-first-calc-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>Although the <code>translate</code> values are still multiplied, they&#39;re multiplied by a constant 3, the end <code>scale</code>, rather than a changing <code>scale</code> value. The result is a steady move towards the target. Each 1px shift in the <code>translate</code> value results in a 1px shift on screen.</p>
<p>Unfortunately, this format is harder to tweak in DevTools, but you can fix that with a bit of <code>calc</code>!</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">--scale</span><span class="token punctuation">:</span> 3<span class="token punctuation">;</span>
  <span class="token property">--x</span><span class="token punctuation">:</span> -33.1%<span class="token punctuation">;</span>
  <span class="token property">--y</span><span class="token punctuation">:</span> 20.2%<span class="token punctuation">;</span>

  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translate</span><span class="token punctuation">(</span>
      <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--x<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--y<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">)</span>
    <span class="token function">scale</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Or even, split the <code>transform</code> into two separate properties:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">--scale</span><span class="token punctuation">:</span> 3<span class="token punctuation">;</span>
  <span class="token property">--x</span><span class="token punctuation">:</span> -33.1%<span class="token punctuation">;</span>
  <span class="token property">--y</span><span class="token punctuation">:</span> 20.2%<span class="token punctuation">;</span>

  <span class="token property">scale</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">translate</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--x<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--y<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>When you use the <code>scale</code> and <code>translate</code> properties separately, the <code>translate</code> is always applied first—which just happens to be the order we want.</p>
<p>Job done!</p>
<h2 id="but-wait-why-did-rotate0-fix-it"><a href="#but-wait-why-did-rotate0-fix-it">But wait, why did rotate(0) fix it?</a></h2>
<p>Back at the start of the article (remember that?), I mentioned that the animation could be &#39;fixed&#39; by making the starting transform <code>rotate(0)</code>:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Even though the <code>scale</code> and <code>translate</code> are in the wrong order, we get the animation we want. What gives? Well, I don&#39;t actually recommend using this &#39;fix&#39;, because it only &#39;works&#39; by hitting an edge case in the CSS spec.</p>
<p>Let&#39;s go through the algorithm again, but this time with the <code>rotate(0)</code> transform:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>As before, it pads out the values with <code>none</code> so they have the same number of components:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Then, as before, it tries to convert each component pair to use a common function that can express both types of value. However, it can&#39;t. <code>rotate</code> and <code>scale</code> are considered too different to be converted into a common type.</p>
<p>When this happens, it &#39;recovers&#39; by converting those values, and all following values, to a single matrix:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">matrix</span><span class="token punctuation">(</span>1<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 1<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">matrix</span><span class="token punctuation">(</span>3<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 3<span class="token punctuation">,</span> -673.75<span class="token punctuation">,</span> 231.904<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>And then it animates them as it would two matrices. The &#39;incorrect&#39; order of the <code>scale</code> and <code>translate</code> is lost, and the <code>translate</code> is already pre-multiplied by the <code>scale</code>. By coincidence, it animates exactly the way we want it to.</p>
<h2 id="bonus-round-scale-vs-3d-translate"><a href="#bonus-round-scale-vs-3d-translate">Bonus round: scale vs 3D translate</a></h2>
<p>In this article, we&#39;ve been animating <code>scale</code> to achieve the effect of &#39;zooming in&#39;. But, depending on the effect you want, you could use a 3D translate instead.</p>
<p>When you animate <code>scale</code>, the result is that the width and height of the target changes linearly throughout the animation (although, as before, easing can be applied). This feels similar to a camera zoom effect.</p>
<p>However, you may want an effect that&#39;s more like the object moving towards the camera, or the camera moving towards the object. To achieve this, you don&#39;t want the visual size of the object to change linearly.</p>
<p>This is because, when a far-away object moves by 1 metre, the amount of space it takes up in your field of view doesn&#39;t change much. But, when a close-up object moves by 1 metre, the amount of space it takes up in your field of view changes a lot. This is the effect of perspective.</p>
<p>So, instead of using <code>scale</code>, let&#39;s use the <code>perspective</code> property, and a 3D transform:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.container</span> <span class="token punctuation">{</span>
  <span class="token property">perspective</span><span class="token punctuation">:</span> 1000px<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">translate</span><span class="token punctuation">:</span> -33.1% 20.2% 666.666px<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>The rather demonic translate-z value was calculated by converting the <code>scale</code> to a translate-z value, using the following formula:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token function-variable function">scaleToTranslateZ</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">scale<span class="token punctuation">,</span> perspective</span><span class="token punctuation">)</span> <span class="token operator">=></span>
  <span class="token punctuation">(</span>perspective <span class="token operator">*</span> <span class="token punctuation">(</span>scale <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">/</span> scale<span class="token punctuation">;</span></code></pre></div><p>And here&#39;s the result:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-z-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>Ok, it&#39;s subtle. Here&#39;s the <code>scale</code> version again for comparison:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-first-calc-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure>

<script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script>

<p>It&#39;s mostly noticeable on the zoom-out part of the animation. The 3D version feels like it starts much faster than the <code>scale</code> version. Personally, I think the <code>scale</code> version feels better, since the intention is more of a &#39;zoom&#39; than a &#39;move&#39;. But, it&#39;s good to know the differences, so you can choose the right one for your desired effect.</p>
<small>

<p>Thanks to <a href="https://ohhelloana.blog/">Ana Rodrigues</a> for feedback that helped this make a lot more sense.</p>
</small>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Firefox + custom elements + iframes bug]]></title>
        <id>https://jakearchibald.com/2025/firefox-custom-elements-iframes-bug/</id>
        <link href="https://jakearchibald.com/2025/firefox-custom-elements-iframes-bug/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-02-14T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[A tricksy Firefox bug and how to work around it.]]></summary>
        <content type="html"><![CDATA[<p>Over at Shopify we&#39;ve been building a bunch of web components to use internally and in third party contexts. All of a sudden, we found some strange errors in our logs, all from Firefox. This is the post I wish existed when we discovered it.</p>
<h2 id="the-bug"><a href="#the-bug">The bug</a></h2>
<p>The bug happens when a custom element (or web component) is moved to a document from another JavaScript Realm <em>[spooky noises]</em>. A Realm is a separate JavaScript context with its own global, own implementation of <code>Array</code> etc etc. An iframe or popup window provides a document in a new JavaScript Realm.</p>
<p>The result of the bug is that the element&#39;s custom prototype is lost, and things like instance methods disappear.</p>
<p>Ok ok, sorry, I&#39;m trying to hit all the terms that people might search for to find a fix for this issue. Anyway, here&#39;s a simple custom element:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
  <span class="token function">say</span><span class="token punctuation">(</span><span class="token parameter">message</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'hello!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'goodbye!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'my-element'</span><span class="token punctuation">,</span> MyElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>And I&#39;m going to create an instance of the element, and put it in an iframe:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Create the iframe</span>
<span class="token keyword">const</span> iframe <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'iframe'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>iframe<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Create the element, and put it in the iframe</span>
<span class="token keyword">const</span> myElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'my-element'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
iframe<span class="token punctuation">.</span>contentDocument<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>myElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This fails in Firefox with &quot;<code>this.say</code> is not a function&quot;, within <code>connectedCallback</code>. In fact, Firefox has lost all of the instance methods of the custom element. It&#39;s &#39;downgraded&#39; to an instance of <code>HTMLElement</code> in the iframe Realm, rather than <code>MyElement</code>.</p>
<p>It&#39;s kinda funny, because the error happens within <code>connectedCallback</code>, which is an instance method, but even <em>that</em> instance method has gone. I assume the calling of <code>connectedCallback</code> was queued up before the prototype was lost.</p>
<p>If I put the element in the main document before moving it to the iframe, it fails in <code>disconnectedCallback</code> for the same reason.</p>
<p>Unfortunately this has been <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1502814">a known issue for 6 years</a>.</p>
<h2 id="the-fix"><a href="#the-fix">The fix</a></h2>
<p>The fix is kinda simple. Just… put the prototype back. As in, make it an instance of <code>MyElement</code> again.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">CustomElementBase</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
  <span class="token comment">// This is called when the element is moved to a new document.</span>
  <span class="token comment">// This is where we solve the bug if the element is moved to an iframe</span>
  <span class="token comment">// without first being put into the main document.</span>
  <span class="token function">adoptedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token comment">// This is called when the element is disconnected from a document.</span>
  <span class="token comment">// This happens whenever the element is moved around the DOM,</span>
  <span class="token comment">// but it also happens when the element is moved to a new document.</span>
  <span class="token comment">// This happens before adoptedCallback,</span>
  <span class="token comment">// so we need to fix it here,</span>
  <span class="token comment">// to avoid the bug in subclass disconnectedCallback calls.</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// Return if everything looks as expected.</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>element <span class="token keyword">instanceof</span> <span class="token class-name">CustomElementBase</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>

  <span class="token comment">// Otherwise, get the intended constructor…</span>
  <span class="token keyword">const</span> constructor <span class="token operator">=</span> customElements<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>element<span class="token punctuation">.</span>tagName<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token comment">// …and set the prototype.</span>
  Object<span class="token punctuation">.</span><span class="token function">setPrototypeOf</span><span class="token punctuation">(</span>element<span class="token punctuation">,</span> constructor<span class="token punctuation">.</span>prototype<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Thanks to my colleague <a href="https://github.com/frehner">Anthony Frehner</a> who realised <code>customElements.get</code> is a simple way to get the original constructor back, rather than the mad <code>WeakMap</code> hack I was using.</p>
<p>Now, make sure your custom elements extend this base class, and ensure you call <code>super</code> methods:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">CustomElementBase</span> <span class="token punctuation">{</span>
  <span class="token function">say</span><span class="token punctuation">(</span><span class="token parameter">message</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">.</span>connectedCallback<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'hello!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">.</span>disconnectedCallback<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'goodbye!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>And that&#39;s it! The bug is undone, and everything works as expected.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[How should &lt;selectedoption&gt; work?]]></title>
        <id>https://jakearchibald.com/2024/how-should-selectedoption-work/</id>
        <link href="https://jakearchibald.com/2024/how-should-selectedoption-work/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2024-10-18T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[It&#39;s part of the new customisable `&lt;select&gt;`, but there are some tricky details.]]></summary>
        <content type="html"><![CDATA[<p>We&#39;re finally getting a way to fully style &amp; customise <code>&lt;select&gt;</code> elements! But there&#39;s a detail I&#39;d like everyone&#39;s opinion on.</p>
<p><strong>Update:</strong> Your feedback was heard, and folks have agreed to change the behaviour here. See <a href="#update">the update</a> below.</p>
<h2 id="a-brief-intro-to-customisable-select"><a href="#a-brief-intro-to-customisable-select">A brief intro to customisable <code>&lt;select&gt;</code></a></h2>
<p>If you want to hear about it in depth, <a href="https://offthemainthread.tech/episode/stylable-select-element/">I talked about it on OTMT</a>, and <a href="https://developer.chrome.com/blog/rfc-customizable-select">there&#39;s a great post by Una Kravets</a>. But here&#39;s a whirlwind tour:</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
  <span class="token comment">/* Opt in to the customisable mode */</span>
  <span class="token selector">select,
  ::picker(select)</span> <span class="token punctuation">{</span>
    <span class="token property">appearance</span><span class="token punctuation">:</span> base-select<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>select</span><span class="token punctuation">></span></span>
  <span class="token comment">&lt;!--
    The button is the thing that appears on the page.
    It's what you click to open the popover menu.
    You can style it and control its content as you would any other button.
  --></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token punctuation">></span></span>
    …
    <span class="token comment">&lt;!-- We'll get to this later --></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>selectedoption</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>selectedoption</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span>

  <span class="token comment">&lt;!--
    Any other content, which can include divs, images, etc etc,
    is displayed in the popover menu when the button is clicked.
    You can style these elements however you like.
  --></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">></span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">></span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">></span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>select</span><span class="token punctuation">></span></span></code></pre></div><p>And that&#39;s as much background as you need for the rest of this post.</p>
<h2 id="what-about-selectedoption"><a href="#what-about-selectedoption">What about <code>&lt;selectedoption&gt;</code>?</a></h2>
<p><code>&lt;selectedoption&gt;</code> automatically displays the currently selected <code>&lt;option&gt;</code> within the <code>&lt;button&gt;</code> that opens the popover menu.</p>
<p>It&#39;s entirely optional, so if you wanted to manually update the content of the <code>&lt;button&gt;</code> when the selected <code>&lt;option&gt;</code> changes, you can, and you get a lot more control that way. But <code>&lt;selectedoption&gt;</code> is much easier, and works without JavaScript.</p>
<p>When the selected <code>&lt;option&gt;</code> changes, it clears the contents of the <code>&lt;selectedoption&gt;</code>, takes a clone of the contents of the newly selected <code>&lt;option&gt;</code>, and inserts it into the <code>&lt;selectedoption&gt;</code>.</p>
<p>This is kinda new and weird behaviour. <a href="/2024/attributes-vs-properties/#attributes-should-be-for-configuration">I recently wrote that I didn&#39;t like elements that modify themselves</a>, as I think the light DOM should have a single owner. But, I can&#39;t think of a better way to do this, given:</p>
<ul>
<li>The content needs to render in two places at once - in the button and in the menu.</li>
<li>The selected option needs to be able to be styled differently to the equivalent content in the <code>&lt;option&gt;</code>.</li>
<li>The solution must work without JavaScript.</li>
</ul>
<h3 id="but-there-are-limitations"><a href="#but-there-are-limitations">But there are limitations</a></h3>
<p>This is a clone in the <code>el.cloneNode(true)</code> sense. That means it&#39;s a copy of the tree, including attributes, but not including properties, event listeners, or any other internal state. So, if the selected option contains a <code>&lt;canvas&gt;</code>, the clone will be a blank canvas. An <code>&lt;iframe&gt;</code> will reload using the <code>src</code> attribute. CSS animations will appear to start over, since they&#39;re newly constructed elements. Custom elements will be constructed afresh, and may have different internal state to the cloned element.</p>
<p>As far as I can tell, in most cases this will be fine, as the kinds of elements you&#39;d typically use in an <code>&lt;option&gt;</code> are fully configurable via attributes.</p>
<h2 id="but-what-if-the-selected-option-is-modified"><a href="#but-what-if-the-selected-option-is-modified">But what if the selected <code>&lt;option&gt;</code> is modified?</a></h2>
<p>This is the bit I want your opinion on. I&#39;m going to present a series of options, and point out potential limitations/gotchas/issues with each. Not all of these issues are equal, and you may even feel some of them are features rather than bugs (I certainly do).</p>
<p>Editing the content of <code>&lt;option&gt;</code> elements isn&#39;t super common, but it&#39;s possible, so we need to define what happens. It might happen when:</p>
<ul>
<li>Enhancing options with additional data. For example, you might discover asynchronously that an option is &quot;almost sold out&quot;, and want to display that information in the <code>&lt;option&gt;</code>.</li>
<li>Dynamically modifying styles in response to interaction. Most animation libraries modify <code>element.style</code>, which also updates the <code>style</code> attribute.</li>
<li>Going from a &#39;loading&#39; state to a &#39;loaded&#39; state.</li>
</ul>
<p>For example, imagine a React app like this:</p>
<div class="code-example"><pre class="language-jsx"><code><span class="token keyword">function</span> <span class="token function">CustomSelect</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> options <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">return</span> <span class="token punctuation">(</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>select</span><span class="token punctuation">></span></span><span class="token plain-text">
      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token punctuation">></span></span><span class="token plain-text">
        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>selectedoption</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>selectedoption</span><span class="token punctuation">></span></span><span class="token plain-text">
      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><span class="token plain-text">

      </span><span class="token punctuation">{</span>options<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">option</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>option<span class="token punctuation">.</span>value<span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">
          </span><span class="token punctuation">{</span>option<span class="token punctuation">.</span>icon <span class="token operator">&amp;&amp;</span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>option<span class="token punctuation">.</span>icon<span class="token punctuation">}</span></span> <span class="token attr-name">alt</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>option<span class="token punctuation">.</span>iconAlt<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">}</span><span class="token plain-text">
          </span><span class="token punctuation">{</span>option<span class="token punctuation">.</span>text<span class="token punctuation">}</span><span class="token plain-text">
        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span>
      <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text">
    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>select</span><span class="token punctuation">></span></span>
  <span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">[</span>options<span class="token punctuation">,</span> setOptions<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">[</span>
    <span class="token punctuation">{</span>
      <span class="token literal-property property">text</span><span class="token operator">:</span> <span class="token string">'Loading…'</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    <span class="token comment">// Fetch option data and call setOptions</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">CustomSelect</span></span> <span class="token attr-name">options</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>options<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Because the above example doesn&#39;t give each <code>&lt;option&gt;</code> a meaningful <code>key</code>, React will modify the first &#39;loading&#39; <code>&lt;option&gt;</code> to become the first real <code>&lt;option&gt;</code> when the data loads. It&#39;s better to use <code>key</code> in this situation, which would cause React to create a new <code>&lt;option&gt;</code> element, but not everyone does this.</p>
<p>You might see a similar pattern to above when updating <code>&lt;select&gt;</code> depending on a choice made earlier in a form.</p>
<p>So what should happen?</p>
<h3 id="option-nothing-by-default-but-provide-a-way-to-trigger-an-update"><a href="#option-nothing-by-default-but-provide-a-way-to-trigger-an-update">Option: Nothing by default, but provide a way to trigger an update</a></h3>
<p>When an <code>&lt;option&gt;</code> becomes selected, the content of <code>&lt;selectedoption&gt;</code> is replaced with a clone of the selected <code>&lt;option&gt;</code>&#39;s content. If the content of the selected <code>&lt;option&gt;</code> is later modified, it would become out of sync with the <code>&lt;selectedoption&gt;</code> element.</p>
<p>A method like <code>selectedOption.resetContent()</code> would cause the content to be replaced with a fresh clone of the selected <code>&lt;option&gt;</code>&#39;s content. Developers would have to call this if they&#39;ve updated the <code>&lt;option&gt;</code>&#39;s content in a way that they want mirrored in the <code>&lt;selectedoption&gt;</code>.</p>
<p>Any manual modifications to the contents of <code>&lt;selectedoption&gt;</code> will be overwritten the next time the selected option changes to another <code>&lt;option&gt;</code>, or when <code>selectedOption.resetContent()</code> is called.</p>
<h3 id="option-automatically-reset-the-content-when-anything-in-the-selected-option-changes"><a href="#option-automatically-reset-the-content-when-anything-in-the-selected-option-changes">Option: Automatically reset the content when anything in the selected <code>&lt;option&gt;</code> changes</a></h3>
<p>Whenever the tree in the selected <code>&lt;option&gt;</code> changes, as in a node is added, removed, or attributes change in any way, the content of the <code>&lt;selectedoption&gt;</code> is replaced with a fresh clone of the selected <code>&lt;option&gt;</code>&#39;s content.</p>
<p>This would be a <em>full</em> clone of the <code>&lt;option&gt;</code>&#39;s content. So even if you deliberately only changed one attribute on one element within the selected <code>&lt;option&gt;</code>, every element in the <code>&lt;selectedoption&gt;</code> would be replaced with a fresh clone. This would cause state to be reset in those elements, and things like CSS animations within <code>&lt;selectedoption&gt;</code> would appear to restart.</p>
<p>Since the cloning is performed synchronously, it will probably happen more than you expect. In the above React example where <code>&lt;option&gt;Loading…&lt;/option&gt;</code> is changed to an option with an icon, that&#39;s three changes within the selected <code>&lt;option&gt;</code>:</p>
<ol>
<li>The <code>&lt;img&gt;</code> is inserted (it&#39;s already been given the <code>alt</code> attribute).</li>
<li>The text is updated.</li>
<li>The <code>src</code> of the <code>&lt;img&gt;</code> is updated.</li>
</ol>
<p>So that&#39;s three times the content of the <code>&lt;selectedoption&gt;</code> is replaced with a fresh clone of the selected <code>&lt;option&gt;</code>&#39;s content, and this is a really basic example.</p>
<p>What about this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get the selected &lt;option></span>
<span class="token keyword">const</span> selectedOption <span class="token operator">=</span> select<span class="token punctuation">.</span>selectedOptions<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token comment">// Move the first child to the end</span>
selectedOption<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>selectedOption<span class="token punctuation">.</span>firstChild<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Well, that&#39;s two clones of the selected <code>&lt;option&gt;</code>&#39;s content, because an element &#39;move&#39; is actually two tree modifications: a remove followed by an insert.</p>
<p>If you change 10 styles on an element within the selected <code>&lt;option&gt;</code> via <code>element.style</code>, each change updates the style attribute, so that&#39;s 10 times the content of the <code>&lt;selectedoption&gt;</code> is replaced with a fresh clone of the selected <code>&lt;option&gt;</code>&#39;s content.</p>
<p>If you&#39;re using an animation library to do something fancy within one of the options, they tend to modify <code>element.style</code> per frame. So that means the content of <code>&lt;selectedoption&gt;</code> is being entirely rebuilt every frame, or more likely, many times per frame.</p>
<p>There may be cases where you don&#39;t want a change in the <code>&lt;option&gt;</code> to be reflected in the <code>&lt;selectedoption&gt;</code>. Since they&#39;re independent elements, you can give each independent <code>:hover</code> states via CSS. But, if you want to do something much fancier involving JavaScript, which modifies <code>element.style</code> on <code>mouseenter</code>, that will appear to be mirrored from the selected <code>&lt;option&gt;</code> to the <code>&lt;selectedoption&gt;</code>, which may not be your intent, because only the <code>&lt;option&gt;</code> is being hovered over.</p>
<p>This could actually become more of an issue in future. Right now, when you click on a <code>&lt;details&gt;</code> element, it becomes <code>&lt;details open&gt;</code> – it modifies its own attributes. If you had one of those in a selected <code>&lt;option&gt;</code> and the user clicked on it, the one in the <code>&lt;selectedoption&gt;</code> would appear to open too (via cloning since the attribute changed). Now, having a <code>&lt;details&gt;</code> in an <code>&lt;option&gt;</code> doesn&#39;t really make sense, but since this pattern is becoming more popular on the web platform, it may appear on an element that you would use in an <code>&lt;option&gt;</code>.</p>
<p>There isn&#39;t a way to prevent changes from mirroring to <code>&lt;selectedoption&gt;</code>. The only way around it is to avoid using <code>&lt;selectedoption&gt;</code> and doing things manually.</p>
<p>Also, this automatic &#39;mirroring&#39; is one-way. If you manually alter content in the <code>&lt;selectedoption&gt;</code>, it won&#39;t cause the content in the selected <code>&lt;option&gt;</code> to be updated. Your manual changes in the <code>&lt;selectedoption&gt;</code> will be overwritten the next time the cloning operation occurs.</p>
<h3 id="option-automatically-reset-the-content-when-anything-in-the-selected-option-changes-debounced"><a href="#option-automatically-reset-the-content-when-anything-in-the-selected-option-changes-debounced">Option: Automatically reset the content when anything in the selected <code>&lt;option&gt;</code> changes… debounced</a></h3>
<p>As above, but when the content of the selected <code>&lt;option&gt;</code> changes, the content of the <code>&lt;selectedoption&gt;</code> is replaced with a fresh clone after a microtask. This would always be before the next render.</p>
<p>This reduces the amount of cloning significantly. All the examples I gave above would only trigger a single clone of the selected <code>&lt;option&gt;</code>&#39;s content.</p>
<p>However, it means that:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get the selected &lt;option></span>
<span class="token keyword">const</span> selectedOption <span class="token operator">=</span> select<span class="token punctuation">.</span>selectedOptions<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token comment">// Get the &lt;selectedoption></span>
<span class="token keyword">const</span> selectedOptionMirror <span class="token operator">=</span> select<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'selectedoption'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

selectedOption<span class="token punctuation">.</span>textContent <span class="token operator">=</span> <span class="token string">'New text'</span><span class="token punctuation">;</span>

<span class="token comment">// This may not be true yet, because the mirroring is delayed.</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>selectedOption<span class="token punctuation">.</span>textContent <span class="token operator">===</span> selectedOptionMirror<span class="token punctuation">.</span>textContent<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Also, you still have the behaviours where:</p>
<ul>
<li>Changing one inner element of the selected <code>&lt;option&gt;</code> causes all elements in the <code>&lt;selectedoption&gt;</code> to be replaced.</li>
<li>Changes are mirrored even if you don&#39;t want them to be.</li>
<li>Mirroring is one-way.</li>
</ul>
<h3 id="option-perform-targeted-dom-changes-when-something-in-the-selected-option-changes"><a href="#option-perform-targeted-dom-changes-when-something-in-the-selected-option-changes">Option: Perform targeted DOM changes when something in the selected <code>&lt;option&gt;</code> changes</a></h3>
<p>When the <code>&lt;option&gt;</code> becomes selected, the content of the <code>&lt;selectedoption&gt;</code> is replaced with a clone of the selected <code>&lt;option&gt;</code>&#39;s content. However, the browser maintains a link between each of the elements and their respective clones. If you modify an attribute on an original element, the same attribute on its clone is updated, and only that attribute. This means that changes on one element in the selected <code>&lt;option&gt;</code> won&#39;t cause everything in the <code>&lt;selectedoption&gt;</code> to &#39;reset&#39;, such as CSS animations.</p>
<p>If a new element is introduced into the selected <code>&lt;option&gt;</code>, it will be cloned and inserted into the equivalent position in the <code>&lt;selectedoption&gt;</code>.</p>
<p>Changes will be performed synchronously, but the changes are just a repeat of your specific action. Your changes run twice, once in the selected <code>&lt;option&gt;</code>, and once in the <code>&lt;selectedoption&gt;</code>.</p>
<p>However, when the selected <code>&lt;option&gt;</code> changes to another <code>&lt;option&gt;</code>, the content of the <code>&lt;selectedoption&gt;</code> is fully replaced with a fresh clone of the newly selected <code>&lt;option&gt;</code>&#39;s content.</p>
<p>You still have the behaviours where:</p>
<ul>
<li>Changes are mirrored even if you don&#39;t want them to be.</li>
<li>Mirroring is one-way.</li>
</ul>
<p>The one-way mirroring behaviour is different to the other options. In the &#39;clone&#39; options, any change to the selected <code>&lt;option&gt;</code> will cause the content in the <code>&lt;selectedoption&gt;</code> to &#39;reset&#39;, as the content is completely replaced with a fresh clone. Whereas in this option, since the DOM changes are targeted, if you manually modify the <code>&lt;selectedoption&gt;</code> content, it&#39;s more like a fork.</p>
<p>For example, inserts will be done using an internal version of <code>element.insertBefore</code>, as in &quot;insert <code>node</code> into <code>element</code> before <code>referenceNode</code>&quot;. If the content of <code>&lt;selectedoption&gt;</code> has been manually altered, it&#39;s possible this change will fail because <code>referenceNode</code> is no longer in <code>element</code>, in which case the <code>node</code> won&#39;t be inserted.</p>
<h2 id="so-what-do-you-think"><a href="#so-what-do-you-think">So, what do you think?</a></h2>
<p>This is being actively discussed <a href="https://github.com/whatwg/html/issues/10520">in the HTML spec</a>, but I want a wider set of developers to have their opinions heard on this.</p>
<p>What should happen? Which options do you like? Which do you hate? Let me know what you think in the comments, social networks, <a href="https://news.ycombinator.com/item?id=41878515">Hacker News</a>, or wherever else you can get my attention. I&#39;ll present this at the next OpenUI meeting.</p>
<h2 id="update"><a href="#update">Update</a></h2>
<p>This was <a href="https://github.com/openui/open-ui/issues/825#issuecomment-2436124668">discussed in an OpenUI meeting</a>, and the consensus was to go with the first option in this post: nothing by default, but provide a way to trigger an update.</p>
<p>Thank you for all the feedback!</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Video with alpha transparency on the web]]></title>
        <id>https://jakearchibald.com/2024/video-with-transparency/</id>
        <link href="https://jakearchibald.com/2024/video-with-transparency/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-C4aIqxe7.png" type="image/png"/>
        <updated>2024-08-05T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[It&#39;s better to do it yourself.]]></summary>
        <content type="html"><![CDATA[<p>I&#39;ve been helping some teams at Shopify improve page load performance, and the issue of &#39;videos with an alpha channel&#39; kept coming up, where videos of UI mocks needed to be composited on top of inconsistent backgrounds, such as larger CSS backgrounds.</p>
<p>Often a good solution here is to create the animation using web technologies, but sometimes video is a better solution for consistent frame rates, and allows for effects like motion blur which don&#39;t currently exist on the web.</p>
<style>
  .checkd {
    background: #0000004a url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path d="M1 2V0h1v1H0v1z" fill-opacity=".05"/></svg>');
    background-size: 20px 20px;
    background-attachment: fixed;
  }

  stacked-alpha-video {
    display: block;
    width: 100%;
  }

  stacked-alpha-video video {
    display: none;
  }
</style>

<figure class="full-figure max-figure checkd">
<stacked-alpha-video class="main-demo" style="aspect-ratio: 1597 / 833">
  <video autoplay crossorigin muted playsinline loop>
    <source
      src="/c/split-av1-q7LusocC.mp4"
      type="video/mp4; codecs=av01.0.08M.08.0.110.01.01.01.1"
    />
    <source src="/c/split-hevc-CRJ4ajmf.mp4" type="video/mp4; codecs=hvc1.1.6.H120.b0" />
  </video>
</stacked-alpha-video>
<figcaption>A Shopify UI video with transparency (click to pause)</figcaption>
</figure>

<script>
  const mainDemo = document.querySelector('.main-demo');
  const mainDemoVideo = mainDemo.firstElementChild;

  mainDemo.onclick = () => {
    if (mainDemoVideo.paused) mainDemoVideo.play();
    else mainDemoVideo.pause();
  };
</script>

<p>I didn&#39;t know much about it, so I dug in to try and find the most robust and efficient way to do it. I thought it was going to be a nice little I-can-do-this-while-jetlagged hackday project, but it&#39;s way more complicated than I thought. It turns out, the &#39;native&#39; ways of doing it are inefficient and buggy. If you handle the transparency yourself, you avoid these bugs, and serve a file that&#39;s half the size, or less.</p>
<p>If you just want the solution, <a href="https://www.npmjs.com/package/stacked-alpha-video">here&#39;s <code>&lt;stacked-alpha-video&gt;</code></a>, an NPM package to handle the playback of these videos.</p>
<p>Otherwise, here&#39;s what I discovered, and the many bugs I filed along the way.</p>
<h2 id="native-support-for-transparency-in-web-compatible-video-formats"><a href="#native-support-for-transparency-in-web-compatible-video-formats">Native support for transparency in web-compatible video formats</a></h2>
<p>Web-friendly video formats have supported transparency for 15 years. So by now it&#39;d be well-supported and easy to use, right? Right?</p>
<p>Right?</p>
<h3 id="avif-aint-it"><a href="#avif-aint-it">AVIF ain&#39;t it</a></h3>
<p>AV1 is a great video format in terms of the compression ratio, and the encoder is great for a variety of content. However, surprisingly, it doesn&#39;t support transparency.</p>
<p>AVIF is an image format built from AV1. AVIF <em>does</em> support transparency. Also, &#39;animated AVIF&#39; is a thing. It&#39;s stored as two AV1 streams, where the additional stream is a luma-only (black &amp; white) video representing the alpha channel.</p>
<p>Taking the example at the top of this post, I can get the size down to 504 kB with acceptable loss.</p>
<p>Here&#39;s a demo, but… don&#39;t get your hopes up:</p>
<p><button class="btn avif-demo-show-btn">Show AVIF demo</button></p>
<div class="avif-demo"></div>

<p><button class="btn avif-demo-hide-btn" style="display: none">Hide AVIF demo</button></p>
<script>
  const avifDemoContainer = document.querySelector('.avif-demo');
  const avifDemoShowBtn = document.querySelector('.avif-demo-show-btn');
  const avifDemoHideBtn = document.querySelector('.avif-demo-hide-btn');

  avifDemoShowBtn.onclick = () => {
    avifDemoContainer.innerHTML = `
      <figure class="full-figure max-figure checkd">
        <img src="/c/ose-Co5tgIth.avif" alt="AVIF demo" />
        <figcaption>Janky & buggy AVIF demo</figcaption>
      </figure>
    `;

    avifDemoShowBtn.style.display = 'none';
    avifDemoHideBtn.style.display = '';
  };

  avifDemoHideBtn.onclick = () => {
    avifDemoContainer.innerHTML = '';
    avifDemoShowBtn.style.display = '';
    avifDemoHideBtn.style.display = 'none';
  };
</script>

<p>Given that <a href="https://caniuse.com/avif">AVIF is well supported</a>, it sounds like the ideal solution. But…</p>
<h4 id="it-doesnt-work-in-safari"><a href="#it-doesnt-work-in-safari">It doesn&#39;t work in Safari</a></h4>
<p>Although Safari supports AVIF, and supports transparency in AVIF, it doesn&#39;t correctly support transparency in an animated AVIF. It looks a real mess, and its horrendously slow.</p>
<p><a href="https://bugs.webkit.org/show_bug.cgi?id=275906">Bug report</a>.</p>
<h4 id="the-performance-is-prohibitively-bad"><a href="#the-performance-is-prohibitively-bad">The performance is prohibitively bad</a></h4>
<p>Chrome and Firefox render the AVIF correctly, but it struggles to hit 60fps even on an ultra high-end laptop. On Android, it struggles to hit even a few frames per second.</p>
<ul>
<li><a href="https://issues.chromium.org/issues/349566435">Chrome bug report</a>.</li>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1909646">Firefox bug report</a>.</li>
</ul>
<h4 id="streaming-is-poor"><a href="#streaming-is-poor">Streaming is poor</a></h4>
<p>When playing content back via <code>&lt;video&gt;</code>, browsers will delay starting playback until it thinks enough is buffered for uninterrupted playback. However, because this is <code>&lt;img&gt;</code> rather than <code>&lt;video&gt;</code>, Chrome will just display frames as soon as it has the data, making playback really choppy on slower connections.</p>
<p>This is really because…</p>
<h4 id="animated-image-formats-are-a-hack"><a href="#animated-image-formats-are-a-hack">Animated image formats are a hack</a></h4>
<p>Im my opinion, animated AVIF doesn&#39;t make sense in a world where AV1 video exists. Even if all the above bugs were fixed, you&#39;d still be unable to:</p>
<ul>
<li>Show browser video playback controls.</li>
<li>Programmatically pause/resume playback, e.g. for accessibility reasons.</li>
<li>Fallback to an alternative video format via <code>&lt;source&gt;</code>.</li>
<li>Include audio.</li>
</ul>
<p>There are benefits to being able to include animated content in an <code>&lt;img&gt;</code>, especially in contexts like forums that support <code>&lt;img&gt;</code> but not <code>&lt;video&gt;</code>. The good news is, Safari solved this back in 2018:</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>whatever.mp4<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre></div><p>The above just works in Safari. You can even use videos as image content in CSS. The bad news is, <a href="https://issues.chromium.org/issues/41359195">Chrome isn&#39;t interested in supporting this</a>. Booooooooo.</p>
<h4 id="encoding-animated-avif"><a href="#encoding-animated-avif">Encoding animated AVIF</a></h4>
<p>For completeness: here&#39;s how to create an animated AVIF using <a href="https://ffmpeg.org/">ffmpeg</a>:</p>
<div class="code-example"><pre class="language-bash"><code><span class="token assign-left variable">INPUT</span><span class="token operator">=</span><span class="token string">"in.mov"</span> <span class="token assign-left variable">OUTPUT</span><span class="token operator">=</span><span class="token string">"out.avif"</span> <span class="token assign-left variable">CRF</span><span class="token operator">=</span><span class="token number">45</span> <span class="token assign-left variable">CRFA</span><span class="token operator">=</span><span class="token number">60</span> <span class="token assign-left variable">CPU</span><span class="token operator">=</span><span class="token number">3</span> <span class="token function">bash</span> <span class="token parameter variable">-c</span> <span class="token string">'ffmpeg -y -i "$INPUT" -color_range tv -pix_fmt:0 yuv420p -pix_fmt:1 gray8 -filter_complex "[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]" -map "[main]:v" -map "[alpha]:v" -an -c:v libaom-av1 -cpu-used "$CPU" -crf "$CRF" -crf:1 "$CRFA" -pass 1 -f null /dev/null &amp;&amp; ffmpeg -y -i "$INPUT" -color_range tv -pix_fmt:0 yuv420p -pix_fmt:1 gray8 -filter_complex "[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]" -map "[main]:v" -map "[alpha]:v" -an -c:v libaom-av1 -cpu-used "$CPU" -crf "$CRF" -crf:1 "$CRFA" -pass 2 "$OUTPUT"'</span></code></pre></div><ul>
<li><code>CRF</code> (0-63): Lower values are higher quality, larger filesize.</li>
<li><code>CRFA</code> (0-63): Like <code>CRF</code>, but for the alpha channel.</li>
<li><code>CPU</code> (0-8): Weirdly, <em>lower</em> values use more CPU, which improves quality, but encodes much slower. I wouldn&#39;t go lower than 3.</li>
</ul>
<h3 id="vp9--hevc-somewhat-works"><a href="#vp9--hevc-somewhat-works">VP9 + HEVC somewhat works</a></h3>
<p>This solution isn&#39;t ideal, and definitely isn&#39;t the most efficient, but it&#39;s the best we&#39;ve got when it comes to native support, meaning it works without JavaScript:</p>
<figure class="full-figure max-figure checkd">
<video controls autoplay crossorigin muted playsinline loop style="aspect-ratio: 1597 / 833; display: block; width: 100%">
  <source type="video/quicktime; codecs=hvc1.1.6.H120.b0" src="/c/hevc-D_-LfvxD.mov" />
  <source type="video/webm; codecs=vp09.00.41.08" src="/c/vp9-BSfBxVP5.webm" />
</video>
<figcaption>A reasonable solution that doesn't need JavaScript</figcaption>
</figure>

<p>This is 1.1 MB in Chrome &amp; Firefox via the VP9 codec, and 3.4 MB in Safari via the HEVC codec. The VP9 is double the size of the AVIF, which shows the generational gap between the codecs.</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span> <span class="token attr-name">playsinline</span> <span class="token attr-name">muted</span> <span class="token attr-name">autoplay</span> <span class="token attr-name">loop</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/quicktime; codecs=hvc1.1.6.H120.b0<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video.mov<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/webm; codecs=vp09.00.41.08<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video.webm<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">></span></span></code></pre></div><p>The HEVC must appear first. Safari supports VP9, but it doesn&#39;t support VP9 with transparency (<a href="https://bugs.webkit.org/show_bug.cgi?id=275908">bug report</a>), so we need to &#39;encourage&#39; Safari to pick the HEVC file over the VP9.</p>
<p>I&#39;m a little worried that a non-Apple device will try to play the HEVC file, but not support transparency (after all, it&#39;s an <a href="https://developer.apple.com/av-foundation/HEVC-Video-with-Alpha-Interoperability-Profile.pdf">Apple extension to the format (pdf)</a>), resulting in broken output. However, I haven&#39;t seen this happen yet.</p>
<p>Also, there are a couple of bugs to be aware of:</p>
<ul>
<li><a href="https://issues.chromium.org/issues/349610465">Chrome Android gets the alpha channel wrong</a>. Depending on how much transparency you use, it might not matter too much. This has been fixed in Canary, but at time of writing, it hasn&#39;t reached stable.</li>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1905878">Playback often stalls on Firefox for Android</a>.</li>
</ul>
<h4 id="encoding-vp9"><a href="#encoding-vp9">Encoding VP9</a></h4>
<p>With <a href="https://ffmpeg.org/">ffmpeg</a>:</p>
<div class="code-example"><pre class="language-bash"><code><span class="token assign-left variable">INPUT</span><span class="token operator">=</span><span class="token string">"in.mov"</span> <span class="token assign-left variable">OUTPUT</span><span class="token operator">=</span><span class="token string">"out.webm"</span> <span class="token assign-left variable">CRF</span><span class="token operator">=</span><span class="token number">45</span> <span class="token assign-left variable">EFFORT</span><span class="token operator">=</span><span class="token string">"good"</span> <span class="token function">bash</span> <span class="token parameter variable">-c</span> <span class="token string">'ffmpeg -y -i "$INPUT" -pix_fmt yuva420p -an -c:v libvpx-vp9 -crf "$CRF" -b:v 0 -deadline "$EFFORT" -threads 4 -lag-in-frames 25 -row-mt 1 -pass 1 -f null /dev/null &amp;&amp; ffmpeg -y -i "$INPUT" -pix_fmt yuva420p -an -c:v libvpx-vp9 -crf "$CRF" -b:v 0 -deadline "$EFFORT" -threads 4 -lag-in-frames 25 -row-mt 1 -pass 2 "$OUTPUT"'</span></code></pre></div><ul>
<li><code>CRF</code> (0-63): Lower values are higher quality, larger filesize.</li>
<li><code>EFFORT</code> (<code>best</code> or <code>good</code>): <code>good</code> is faster, but slightly lower quality.</li>
</ul>
<h4 id="encoding-hevc"><a href="#encoding-hevc">Encoding HEVC</a></h4>
<p>This is the format you need for Apple devices, so it might not surprise you to hear that you can only encode it on MacOS.</p>
<p>In addition, you really need to fork out £50 or whatever for <a href="https://www.apple.com/uk/final-cut-pro/compressor/">Apple&#39;s Compressor</a>.</p>
<p>But even then, the results aren&#39;t great. I don&#39;t think Apple&#39;s Compressor is designed with this kind of content in mind, so you usually end up with a much larger file size than the equivalent VP9.</p>
<p><a href="https://youtu.be/Js4fNuOh1Ac">Here&#39;s a quick video guide to using the Compressor app to encode video with an alpha channel</a>.</p>
<p>If, after forking out £££ for an Apple device, you really really really don&#39;t want to spend £50 on Compressor, you can encode a kinda shitty version using <a href="https://ffmpeg.org/">ffmpeg</a>. Note: this only works on MacOS, as it calls out to the built-in codec.</p>
<div class="code-example"><pre class="language-bash"><code>ffmpeg <span class="token parameter variable">-i</span> in.mov <span class="token parameter variable">-c:v</span> hevc_videotoolbox <span class="token parameter variable">-require_sw</span> <span class="token number">1</span> <span class="token parameter variable">-alpha_quality</span> <span class="token number">0.1</span> <span class="token parameter variable">-tag:v</span> hvc1 <span class="token parameter variable">-q:v</span> <span class="token number">35</span> <span class="token parameter variable">-vf</span> <span class="token string">"premultiply=inplace=1"</span> out.mov</code></pre></div><ul>
<li><code>-q:v</code> (0-100): Quality, where 100 is the highest quality with largest file size.</li>
<li><code>-alpha_quality</code> (0-1): Separate control of the alpha channel quality.</li>
</ul>
<p>The reason this method is worse is because, for whatever reason, it uses the BGRA pixel format. This means the red, green, and blue channels are stored separately. This isn&#39;t very efficient. Video formats tend to use YUV, where brightness (Y) is stored separate to colour (UV). Human eyes are more sensitive to brightness than colour, so this separation means more bits can be spent on the brightness data vs the colour. I&#39;ve <a href="https://trac.ffmpeg.org/ticket/11068">filed a bug to see if this can be fixed</a>. In the meantime, this method will yield around double the file size compared the already-not-great Compressor result.</p>
<p>There&#39;s <a href="https://bitbucket.org/multicoreware/x265_git/issues/577/support-for-alpha-transparency-per-apple">a feature request for the open source &amp; cross-platform x265 codec to support transparency</a>, but it doesn&#39;t seem to be going anywhere.</p>
<h2 id="doing-it-manually"><a href="#doing-it-manually">Doing it manually</a></h2>
<p>AV1 is the most efficient codec we have in browsers, but it doesn&#39;t support transparency. When it&#39;s in an AVIF container, it does, but the performance is prohibitively bad.</p>
<p>So, I thought, what if I split the video in two, making it double height, where the top half is the video without the alpha channel, and the bottom half is the alpha channel represented as brightness?</p>
<figure class="full-figure max-figure checkd">
  <video controls autoplay crossorigin muted playsinline loop style="display: block; width: 100%; max-height: 60vh; aspect-ratio: 665 / 693;">
    <source
      src="/c/split-av1-q7LusocC.mp4"
      type="video/mp4; codecs=av01.0.08M.08.0.110.01.01.01.1"
    />
    <source src="/c/split-hevc-CRJ4ajmf.mp4" type="video/mp4; codecs=hvc1.1.6.H120.b0" />
  </video>
<figcaption>Top half: colour data. Bottom half: alpha data.</figcaption>
</figure>

<p>Then, a WebGL fragment shader can be used to efficiently apply the bottom half as a mask to the top half:</p>
<div class="code-example"><pre class="language-glsl"><code><span class="token comment">// The video frame</span>
<span class="token keyword">uniform</span> <span class="token keyword">sampler2D</span> u_frame<span class="token punctuation">;</span>

<span class="token comment">// The texCoords passed in from the vertex shader.</span>
<span class="token keyword">varying</span> <span class="token keyword">vec2</span> v_texCoord<span class="token punctuation">;</span>

<span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// Calculate the coordinates for the color (top half of the frame)</span>
  <span class="token keyword">vec2</span> colorCoord <span class="token operator">=</span> <span class="token keyword">vec2</span><span class="token punctuation">(</span>v_texCoord<span class="token punctuation">.</span>x<span class="token punctuation">,</span> v_texCoord<span class="token punctuation">.</span>y <span class="token operator">*</span> <span class="token number">0.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token comment">// Calculate the coordinates for the alpha (bottom half of the frame)</span>
  <span class="token keyword">vec2</span> alphaCoord <span class="token operator">=</span> <span class="token keyword">vec2</span><span class="token punctuation">(</span>v_texCoord<span class="token punctuation">.</span>x<span class="token punctuation">,</span> <span class="token number">0.5</span> <span class="token operator">+</span> v_texCoord<span class="token punctuation">.</span>y <span class="token operator">*</span> <span class="token number">0.5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token comment">// Pull the pixel values from the video frame</span>
  <span class="token keyword">vec4</span> color <span class="token operator">=</span> <span class="token function">texture2D</span><span class="token punctuation">(</span>u_frame<span class="token punctuation">,</span> colorCoord<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">float</span> alpha <span class="token operator">=</span> <span class="token function">texture2D</span><span class="token punctuation">(</span>u_frame<span class="token punctuation">,</span> alphaCoord<span class="token punctuation">)</span><span class="token punctuation">.</span>r<span class="token punctuation">;</span>

  <span class="token comment">// Merge the rgb values with the alpha value</span>
  gl_FragColor <span class="token operator">=</span> <span class="token keyword">vec4</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>rgb<span class="token punctuation">,</span> alpha<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>And here it is:</p>
<figure class="full-figure max-figure checkd">
<stacked-alpha-video class="main-demo-2" style="aspect-ratio: 1597 / 833">
  <video autoplay crossorigin muted playsinline loop>
    <source
      src="/c/split-av1-q7LusocC.mp4"
      type="video/mp4; codecs=av01.0.08M.08.0.110.01.01.01.1"
    />
    <source src="/c/split-hevc-CRJ4ajmf.mp4" type="video/mp4; codecs=hvc1.1.6.H120.b0" />
  </video>
</stacked-alpha-video>
<figcaption>A Shopify UI video with transparency (click to pause)</figcaption>
</figure>

<script>
  {
    const mainDemo = document.querySelector('.main-demo-2');
    const mainDemoVideo = mainDemo.firstElementChild;

    mainDemo.onclick = () => {
      if (mainDemoVideo.paused) mainDemoVideo.play();
      else mainDemoVideo.pause();
    };
  }
</script>

<p>For Chrome, Firefox, and Safari on a iPhone 15 Pro or M3 MacBook Pro, this is 460 kB. A huge reduction compared to 1.1 or 3.4 MB for the native version.</p>
<p>In fact, the alpha data in the bottom half of the video only adds 8 kB to the overall size. I guess this is because the content is a single channel (brightness), mostly flat color, and mostly unchanged from frame to frame. Cases where the alpha data changes more frequently, or includes a lot of semi-transparency, will likely contribute more to the overall file size.</p>
<p>Other Apple devices don&#39;t support AV1, so they need an HEVC version at 1.14 MB, which isn&#39;t as good, but still a lot smaller than the 3.4 MB version they&#39;d get for the native version.</p>
<p>The 460 kB AV1 version is even significantly smaller than the 504 kB AVIF. I&#39;m not really sure why. With the AVIF, I encoded with the exact same settings - I even encoded the alpha data lower quality in the AVIF, so in theory it should be at an advantage. I guess the AVIF has overhead by being two separate video streams, whereas the stacked version is one video.</p>
<h3 id="wrapping-it-up-in-a-web-component"><a href="#wrapping-it-up-in-a-web-component">Wrapping it up in a web component</a></h3>
<p>I&#39;ve <a href="https://www.npmjs.com/package/stacked-alpha-video">published a little web component to handle the rendering</a>:</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>stacked-alpha-video</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span> <span class="token attr-name">autoplay</span> <span class="token attr-name">crossorigin</span> <span class="token attr-name">muted</span> <span class="token attr-name">playsinline</span> <span class="token attr-name">loop</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span>
      <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>av1.mp4<span class="token punctuation">"</span></span>
      <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/mp4; codecs=av01.0.08M.08.0.110.01.01.01.1<span class="token punctuation">"</span></span>
    <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hevc.mp4<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/mp4; codecs=hvc1.1.6.H120.b0<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>stacked-alpha-video</span><span class="token punctuation">></span></span></code></pre></div><p>You control playback via the <code>&lt;video&gt;</code>, so you&#39;re in full control over how the video is fetched, and it can also start fetching before JS loads. The web component just handles the rendering.</p>
<p><a href="https://offthemainthread.tech/episode/are-web-components-worth-it/">I&#39;m far from a web component absolutist</a>, but it seemed like the perfect choice here to make the component useable across frameworks, or without a framework.</p>
<p><a href="https://www.npmjs.com/package/stacked-alpha-video">It&#39;s available on NPM</a>, but <a href="https://github.com/jakearchibald/stacked-alpha-video?tab=readme-ov-file#dont-want-to-use-a-web-component">I also export the internals</a>, so you can access the WebGL bits without going via the web component.</p>
<p>The one thing I think it&#39;s missing is being able to use the native <code>&lt;video&gt;</code> controls, so I <a href="https://github.com/whatwg/html/issues/10507">filed an issue for that too</a>.</p>
<h3 id="encoding-the-video"><a href="#encoding-the-video">Encoding the video</a></h3>
<p>Again, ffmpeg is the tool for the job. Here&#39;s the filter:</p>
<div class="code-example"><pre class="language-bash"><code><span class="token parameter variable">-filter_complex</span> <span class="token string">"[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]; [main][alpha]vstack"</span></code></pre></div><p>Breaking it up step by step:</p>
<ol>
<li><code>[0:v]format=pix_fmts=yuva444p[main]</code> convert to a predictable format.</li>
<li><code>[main]split[main][alpha]</code> fork the output.</li>
<li><code>[alpha]alphaextract[alpha]</code> with the &#39;alpha&#39; fork, pull the alpha data out to luma data, creating a black &amp; white view of the transparency.</li>
<li><code>[main][alpha]vstack</code> stack the &#39;main&#39; and &#39;alpha&#39; forks on top of each other.</li>
</ol>
<h4 id="encoding-av1"><a href="#encoding-av1">Encoding AV1</a></h4>
<p>This is the ideal format:</p>
<div class="code-example"><pre class="language-bash"><code><span class="token assign-left variable">INPUT</span><span class="token operator">=</span><span class="token string">"in.mov"</span> <span class="token assign-left variable">OUTPUT</span><span class="token operator">=</span><span class="token string">"av1.mp4"</span> <span class="token assign-left variable">CRF</span><span class="token operator">=</span><span class="token number">45</span> <span class="token assign-left variable">CPU</span><span class="token operator">=</span><span class="token number">3</span> <span class="token function">bash</span> <span class="token parameter variable">-c</span> <span class="token string">'ffmpeg -y -i "$INPUT" -filter_complex "[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]; [main][alpha]vstack" -pix_fmt yuv420p -an -c:v libaom-av1 -cpu-used "$CPU" -crf "$CRF" -pass 1 -f null /dev/null &amp;&amp; ffmpeg -y -i "$INPUT" -filter_complex "[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]; [main][alpha]vstack" -pix_fmt yuv420p -an -c:v libaom-av1 -cpu-used "$CPU" -crf "$CRF" -pass 2 -movflags +faststart "$OUTPUT"'</span></code></pre></div><ul>
<li><code>CRF</code> (0-63): Lower values are higher quality, larger filesize.</li>
<li><code>CPU</code> (0-8): Weirdly, <em>lower</em> values use more CPU, which improves quality, but encodes much slower. I wouldn&#39;t go lower than 3.</li>
</ul>
<h4 id="encoding-hevc-1"><a href="#encoding-hevc-1">Encoding HEVC</a></h4>
<p>Safari on Apple devices will use the AV1 if they have a hardware decoder (iPhone 15 Pro, M3 MacBook Pro), otherwise they need an HEVC alternative. But, since we don&#39;t need native transparency support, we can use the open source &amp; cross-platform x265 codec:</p>
<div class="code-example"><pre class="language-bash"><code><span class="token assign-left variable">INPUT</span><span class="token operator">=</span><span class="token string">"in.mov"</span> <span class="token assign-left variable">OUTPUT</span><span class="token operator">=</span><span class="token string">"hevc.mp4"</span> <span class="token assign-left variable">CRF</span><span class="token operator">=</span><span class="token number">30</span> <span class="token assign-left variable">PRESET</span><span class="token operator">=</span><span class="token string">"veryslow"</span> <span class="token function">bash</span> <span class="token parameter variable">-c</span> <span class="token string">'ffmpeg -y -i "$INPUT" -filter_complex "[0:v]format=pix_fmts=yuva444p[main]; [main]split[main][alpha]; [alpha]alphaextract[alpha]; [main][alpha]vstack" -pix_fmt yuv420p -an -c:v libx265 -preset "$PRESET" -crf "$CRF" -tag:v hvc1 -movflags +faststart "$OUTPUT"'</span></code></pre></div><ul>
<li><code>CRF</code> (0-63): Lower values are higher quality, larger filesize.</li>
<li><code>PRESET</code> (<code>medium</code>, <code>slow</code>, <code>slower</code>, <code>veryslow</code>): The slower you go, the better the output.</li>
</ul>
<p>I find I have to go with a much lower CRF than with the AV1.</p>
<h2 id="aside-limited-range"><a href="#aside-limited-range">Aside: limited range</a></h2>
<p>The ffmpeg examples I&#39;ve given here result in the videos being encoded in &#39;limited range&#39;. This uses 16-235 rather than the full 8bit 0-255. This &#39;feature&#39; exists due to old CRT TVs which would suffer from signal overshooting. Unfortunately, it still hangs around as a kind of default, even to the degree where <a href="https://issues.chromium.org/issues/354454801">Chrome has bugs when handling the full range</a>.</p>
<p>8bit is pretty minimal when it comes to gradients, so this ~15% reduction can result in banding. If this is a problem, you can try encoding to one of the 10bit pixel formats by swapping out <code>format=pix_fmts=yuva444p</code> for <code>format=pix_fmts=yuva444p10le</code>, and changing the <code>pix_fmt</code> to <code>yuv444p10le</code>. I&#39;ll leave that for you to figure out 😀</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Garbage collection and closures]]></title>
        <id>https://jakearchibald.com/2024/garbage-collection-and-closures/</id>
        <link href="https://jakearchibald.com/2024/garbage-collection-and-closures/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-DIApstfY.png" type="image/png"/>
        <updated>2024-07-30T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[GC within a function doesn&#39;t work how I expected]]></summary>
        <content type="html"><![CDATA[<p>Me, <a href="https://twitter.com/DasSurma">Surma</a>, and <a href="https://twitter.com/_developit">Jason</a> were hacking on a thing, and discovered that garbage collection within a function doesn&#39;t quite work how we expected.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">clearTimeout</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

globalThis<span class="token punctuation">.</span>cancelDemo <span class="token operator">=</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>With the above, <code>bigArrayBuffer</code> is leaked forever. I didn&#39;t expect that, because:</p>
<ul>
<li>After a second, the function referencing <code>bigArrayBuffer</code> is no longer callable.</li>
<li>The returned cancel function doesn&#39;t reference <code>bigArrayBuffer</code>.</li>
</ul>
<p>But that doesn&#39;t matter. Here&#39;s why:</p>
<h2 id="javascript-engines-are-reasonably-smart"><a href="#javascript-engines-are-reasonably-smart">JavaScript engines are reasonably smart</a></h2>
<p>This doesn&#39;t leak:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>The function executes, <code>bigArrayBuffer</code> is no longer needed, so it&#39;s garbage collected.</p>
<p>This also doesn&#39;t leak:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>In this case:</p>
<ol>
<li>The engine sees <code>bigArrayBuffer</code> is referenced by inner functions, so it&#39;s kept around. It&#39;s associated with the scope that was created when <code>demo()</code> was called.</li>
<li>After a second, the function referencing <code>bigArrayBuffer</code> is no longer callable.</li>
<li>Since nothing within the scope is callable, the scope can be garbage collected, along with <code>bigArrayBuffer</code>.</li>
</ol>
<p>This also doesn&#39;t leak:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'hello'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">clearTimeout</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

globalThis<span class="token punctuation">.</span>cancelDemo <span class="token operator">=</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>In this case, the engine knows it doesn&#39;t need to retain <code>bigArrayBuffer</code>, as none of the inner-callables access it.</p>
<h2 id="the-problem-case"><a href="#the-problem-case">The problem case</a></h2>
<p>Here&#39;s where it gets messy:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">clearTimeout</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

globalThis<span class="token punctuation">.</span>cancelDemo <span class="token operator">=</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This leaks, because:</p>
<ol>
<li>The engine sees <code>bigArrayBuffer</code> is referenced by inner functions, so it&#39;s kept around. It&#39;s associated with the scope that was created when <code>demo()</code> was called.</li>
<li>After a second, the function referencing <code>bigArrayBuffer</code> is no longer callable.</li>
<li>But, the scope remains, because the &#39;cancel&#39; function is still callable.</li>
<li><code>bigArrayBuffer</code> is associated with the scope, so it remains in memory.</li>
</ol>
<p>I thought engines would be smarter, and GC <code>bigArrayBuffer</code> since it&#39;s no longer referenceable, but that isn&#39;t the case.</p>
<div class="code-example"><pre class="language-js"><code>globalThis<span class="token punctuation">.</span>cancelDemo <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span></code></pre></div><p><em>Now</em> <code>bigArrayBuffer</code> can be GC&#39;d, since nothing within the scope is callable.</p>
<p>This isn&#39;t specific to timers, it&#39;s just how I encountered the issue. For example:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  globalThis<span class="token punctuation">.</span><span class="token function-variable function">innerFunc1</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>

  globalThis<span class="token punctuation">.</span><span class="token function-variable function">innerFunc2</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'hello'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// bigArrayBuffer is retained, as expected.</span>

globalThis<span class="token punctuation">.</span>innerFunc1 <span class="token operator">=</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span>
<span class="token comment">// bigArrayBuffer is still retained, as unexpected.</span>

globalThis<span class="token punctuation">.</span>innerFunc2 <span class="token operator">=</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span>
<span class="token comment">// bigArrayBuffer can now be collected.</span></code></pre></div><p>TIL!</p>
<h2 id="updates"><a href="#updates">Updates</a></h2>
<h3 id="an-iife-is-enough-to-trigger-the-leak"><a href="#an-iife-is-enough-to-trigger-the-leak">An IIFE is enough to trigger the leak</a></h3>
<p>I originally thought this &#39;capturing&#39; of values only happened for functions that outlive the initial execution of the parent function, but that isn&#39;t the case:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>bigArrayBuffer<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  globalThis<span class="token punctuation">.</span><span class="token function-variable function">innerFunc</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'hello'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// bigArrayBuffer is retained, as unexpected.</span></code></pre></div><p>Here, the inner IIFE is enough to trigger the leak.</p>
<h3 id="its-a-cross-browser-issue"><a href="#its-a-cross-browser-issue">It&#39;s a cross-browser issue</a></h3>
<p>This whole thing is an issue across browsers, and is unlikely to be fixed due to performance issues.</p>
<ul>
<li><a href="https://issues.chromium.org/issues/41070945">Chromium issue</a></li>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=894971">Firefox issue</a></li>
<li><a href="https://bugs.webkit.org/show_bug.cgi?id=224077">WebKit issue</a></li>
</ul>
<h3 id="im-not-the-first-to-write-about-this"><a href="#im-not-the-first-to-write-about-this">I&#39;m not the first to write about this</a></h3>
<ul>
<li><a href="https://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html">A low level look at why this happens</a>, by Slava Egorov, back in 2012</li>
<li><a href="https://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html">Meteor engineers discover it</a>, by David Glasser, in 2013</li>
<li><a href="https://schiener.io/2024-03-03/react-closures">This React-centric look at it</a>, by Kevin Schiener, as recently as May 2024.</li>
</ul>
<h3 id="and-no-this-is-not-due-to-eval"><a href="#and-no-this-is-not-due-to-eval">And no, this is not due to <code>eval()</code></a></h3>
<p>Folks on <a href="https://news.ycombinator.com/item?id=41111062">Hacker News</a> and Twitter were quick to point out that this is all because of <code>eval()</code>, but it isn&#39;t.</p>
<p>Eval is tricky, because it means code can exist within a scope that can&#39;t be statically analysed:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> bigArrayBuffer2 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  globalThis<span class="token punctuation">.</span><span class="token function-variable function">innerFunc</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    <span class="token function">eval</span><span class="token punctuation">(</span>whatever<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Are either of the buffers accessed within <code>innerFunc</code>? There&#39;s no way of knowing. But the browser can statically determine that <code>eval</code> is there. This causes a deopt where everything in the parent scopes is retained.</p>
<p>The browser can statically determine this, because <code>eval</code> acts kinda like a keyword. In this case:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> customEval <span class="token operator">=</span> eval<span class="token punctuation">;</span>

<span class="token keyword">function</span> <span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> bigArrayBuffer1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> bigArrayBuffer2 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span><span class="token number">100_000_000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  globalThis<span class="token punctuation">.</span><span class="token function-variable function">innerFunc</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    <span class="token function">customEval</span><span class="token punctuation">(</span>whatever<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token function">demo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>…the deopt doesn&#39;t happen. Because the <code>eval</code> keyword isn&#39;t used directly, <code>whatever</code> will be executed in the global scope, not within <code>innerFunc</code>. This is known as &#39;indirect eval&#39;, and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval">MDN has more on the topic</a>.</p>
<p>This behaviour exists specifically so browsers can limit this deopt to cases that can be statically analysed.</p>
]]></content>
    </entry>
</feed>