<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Danny Holloran</title>
    <description>The latest articles on DEV Community by Danny Holloran (@grimicorn).</description>
    <link>https://dev.to/grimicorn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951431%2Fab822d20-286b-4190-86ce-c6a0bcbb8319.jpeg</url>
      <title>DEV Community: Danny Holloran</title>
      <link>https://dev.to/grimicorn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grimicorn"/>
    <language>en</language>
    <item>
      <title>Svelte Error Boundaries: Contain the Crash Instead of Losing the Page</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Fri, 16 Oct 2026 10:10:37 +0000</pubDate>
      <link>https://dev.to/grimicorn/svelte-error-boundaries-contain-the-crash-instead-of-losing-the-page-59ih</link>
      <guid>https://dev.to/grimicorn/svelte-error-boundaries-contain-the-crash-instead-of-losing-the-page-59ih</guid>
      <description>&lt;p&gt;Every app has that one component. The chart that chokes on a &lt;code&gt;null&lt;/code&gt; in the data, the third-party embed that throws when its script loads late, the dashboard card that assumes an API field will always exist. In a lot of Svelte apps, one of those throwing during render takes the whole page down with it, and your users get a blank screen instead of a page that's 95% fine.&lt;/p&gt;

&lt;p&gt;Svelte 5 has had a fix for this since 5.3: &lt;code&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/code&gt;. It has quietly grown into one of the more useful special elements in the framework, and as of 5.51 it can even handle errors during server-side rendering. If you haven't reached for it yet, here's what it does and where it fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walling Off a Component
&lt;/h2&gt;

&lt;p&gt;A boundary wraps part of your tree. If anything inside throws while rendering or while running an effect, the boundary catches it, removes the broken content, and renders a &lt;code&gt;failed&lt;/code&gt; snippet in its place. The snippet receives the error and a &lt;code&gt;reset&lt;/code&gt; function that rebuilds the contents from scratch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RevenueChart&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./RevenueChart.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;RevenueChart&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card card--error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The revenue chart couldn't load.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Try again&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svelte:boundary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the page keeps rendering normally. Only the chart's slot shows the fallback, and &lt;code&gt;reset&lt;/code&gt; gives the user a way to retry without a full reload.&lt;/p&gt;

&lt;p&gt;If you'd rather report the error than (or as well as) render something, pass an &lt;code&gt;onerror&lt;/code&gt; handler. It gets the same &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;reset&lt;/code&gt; arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Sentry&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sentry/svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ActivityFeed&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ActivityFeed.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;svelte:boundary&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captureException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ActivityFeed&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Activity is unavailable right now.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svelte:boundary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;onerror&lt;/code&gt; itself throws, or you rethrow, the error bubbles to the nearest parent boundary. That lets you build layers: a tight boundary around each risky widget, and a coarse one near the root as the last line of defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Boundaries Don't Catch
&lt;/h2&gt;

&lt;p&gt;This is the part that trips people up. Boundaries catch errors that happen &lt;strong&gt;during rendering and inside effects&lt;/strong&gt;. They do not catch errors in event handlers, inside a &lt;code&gt;setTimeout&lt;/code&gt;, or in other async work that runs outside the render cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- This throw is NOT caught by the boundary --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Click&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Never shown for the click error&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svelte:boundary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the same split React draws, and for the same reason: an event handler failing doesn't leave the UI in a half-rendered state, so there's nothing for a boundary to replace. Handle those errors where they happen, with &lt;code&gt;try/catch&lt;/code&gt; and some local state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pending UI for &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Boundaries also handle the other side of async rendering. With Svelte's &lt;code&gt;await&lt;/code&gt; expressions, you can put an &lt;code&gt;await&lt;/code&gt; directly in your markup, and a boundary's &lt;code&gt;pending&lt;/code&gt; snippet shows while those expressions first resolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$props&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; posts&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"skeleton"&lt;/span&gt; &lt;span class="na"&gt;aria-busy=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Couldn't load profile. Retry?&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svelte:boundary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats. First, &lt;code&gt;pending&lt;/code&gt; only shows on the &lt;strong&gt;initial&lt;/strong&gt; load. Later updates keep the old content on screen, and you use &lt;code&gt;$effect.pending()&lt;/code&gt; if you want a spinner for those. Second, during server-side rendering, a boundary with a &lt;code&gt;pending&lt;/code&gt; snippet renders only that snippet and skips its content, so put boundaries around the slow, non-critical parts of the page and not around the content you need indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries on the Server
&lt;/h2&gt;

&lt;p&gt;Until recently, boundaries did nothing during SSR. An error in any component failed the whole &lt;code&gt;render()&lt;/code&gt; call. Svelte 5.51 changed that with a &lt;code&gt;transformError&lt;/code&gt; option on &lt;code&gt;render&lt;/code&gt; from &lt;code&gt;svelte/server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svelte/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./App.svelte&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transformError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log the real error, with its stack, on the server...&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...and send only a safe, serializable shape to the browser&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong loading this section.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a component inside a boundary with a &lt;code&gt;failed&lt;/code&gt; snippet throws during SSR, &lt;code&gt;transformError&lt;/code&gt; decides what the snippet receives. The return value has to be JSON-serializable because it gets serialized into the page and used to hydrate the &lt;code&gt;failed&lt;/code&gt; snippet in the browser. If the boundary also has &lt;code&gt;onerror&lt;/code&gt;, it fires on hydration with that deserialized object. If &lt;code&gt;transformError&lt;/code&gt; throws, the whole render fails as before.&lt;/p&gt;

&lt;p&gt;The redaction step matters. Server errors can carry database messages, file paths, and stack traces, and the Svelte docs explicitly recommend sanitizing them rather than shipping them to the client.&lt;/p&gt;

&lt;p&gt;If you're on SvelteKit, you don't call &lt;code&gt;render&lt;/code&gt; yourself, so the framework has to wire &lt;code&gt;transformError&lt;/code&gt; up for you. The Svelte docs say that support is coming via SvelteKit's &lt;code&gt;handleError&lt;/code&gt; hook. Check the SvelteKit changelog for your version before you count on server-side boundaries working there. Until then, &lt;code&gt;handleError&lt;/code&gt; and &lt;code&gt;+error.svelte&lt;/code&gt; remain the SvelteKit way to deal with errors in &lt;code&gt;load&lt;/code&gt; and on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Put Them
&lt;/h2&gt;

&lt;p&gt;A good rule: wrap anything whose failure shouldn't cost the user the rest of the page. Third-party embeds, data visualizations, user-generated content renderers, and any widget fed by a flaky API are all prime candidates. Keep boundaries out of trivial components, because a fallback for every button is noise. Start with one root-level boundary plus a few around your riskiest widgets, wire &lt;code&gt;onerror&lt;/code&gt; into whatever error tracking you already use, and read the &lt;a href="https://svelte.dev/docs/svelte/svelte-boundary" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/code&gt; docs&lt;/a&gt; for the full API.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Dependency Cooldowns: Make Your Package Manager Wait Before It Trusts New Code</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 13 Oct 2026 10:11:07 +0000</pubDate>
      <link>https://dev.to/grimicorn/dependency-cooldowns-make-your-package-manager-wait-before-it-trusts-new-code-1hg9</link>
      <guid>https://dev.to/grimicorn/dependency-cooldowns-make-your-package-manager-wait-before-it-trusts-new-code-1hg9</guid>
      <description>&lt;p&gt;Most npm supply-chain attacks follow the same script. Someone phishes a maintainer or steals a publish token, pushes a poisoned patch release, and then waits for every &lt;code&gt;^1.2.3&lt;/code&gt; range on the planet to pick it up on the next install. The malicious version usually gets spotted and pulled within hours. The damage is done in those hours, by machines that installed it before anyone noticed.&lt;/p&gt;

&lt;p&gt;A dependency cooldown attacks that window directly. Instead of installing whatever was published thirty seconds ago, your package manager only accepts versions that have existed for a while. It's a boring idea, and that's why it works: you let the rest of the ecosystem be the canary. Every major package manager now supports it, but each one picked its own config key and, annoyingly, its own unit of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  One idea, four spellings
&lt;/h2&gt;

&lt;p&gt;Here's the same "wait at least a day" policy in each tool.&lt;/p&gt;

&lt;p&gt;pnpm uses minutes, configured in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;. Since pnpm 11 this is &lt;strong&gt;on by default&lt;/strong&gt; at 1440 minutes, so if you're on a current pnpm you already have it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pnpm-workspace.yaml&lt;/span&gt;
&lt;span class="na"&gt;minimumReleaseAge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1440&lt;/span&gt; &lt;span class="c1"&gt;# minutes&lt;/span&gt;
&lt;span class="na"&gt;minimumReleaseAgeExclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@myorg/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm added &lt;code&gt;min-release-age&lt;/code&gt; in CLI 11.10, measured in &lt;strong&gt;days&lt;/strong&gt; and &lt;strong&gt;off by default&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# .npmrc
&lt;/span&gt;&lt;span class="py"&gt;min-release-age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bun uses &lt;strong&gt;seconds&lt;/strong&gt;, under the &lt;code&gt;[install]&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# bunfig.toml&lt;/span&gt;
&lt;span class="nn"&gt;[install]&lt;/span&gt;
&lt;span class="py"&gt;minimumReleaseAge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="c"&gt;# seconds&lt;/span&gt;
&lt;span class="py"&gt;minimumReleaseAgeExcludes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"@types/node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yarn Berry (4.10+) calls it &lt;code&gt;npmMinimalAgeGate&lt;/code&gt;, in minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .yarnrc.yml&lt;/span&gt;
&lt;span class="na"&gt;npmMinimalAgeGate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1440&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minutes, days, seconds, minutes. If you maintain a repo that switches package managers, or you copy a snippet from a blog post written for a different tool, it's very easy to set a 1440-&lt;em&gt;day&lt;/em&gt; cooldown in npm or a 24-&lt;em&gt;second&lt;/em&gt; one in Bun. Leave a comment with the unit next to the number every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that actually matter
&lt;/h2&gt;

&lt;p&gt;The headline setting is simple. The edge cases are where teams get surprised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It applies to transitive dependencies too.&lt;/strong&gt; That's the point: the attack rarely lands in a package you depend on directly. pnpm's docs are explicit that the gate covers the whole tree, which also means a deep dependency like &lt;code&gt;caniuse-lite&lt;/code&gt; (which publishes constantly) can be the thing that trips it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strict vs. fallback.&lt;/strong&gt; In pnpm, if nothing in your requested range is old enough, there are two possible behaviors: fail, or fall back to the too-new version. pnpm's built-in default of 1440 is non-strict for backward compatibility, so it quietly falls back. The moment you set &lt;code&gt;minimumReleaseAge&lt;/code&gt; yourself, &lt;code&gt;minimumReleaseAgeStrict&lt;/code&gt; flips to &lt;code&gt;true&lt;/code&gt; and the install fails instead. That's the behavior you want for security, but be ready for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lockfiles behave differently per tool.&lt;/strong&gt; Bun only applies the gate during new resolution, so entries already in &lt;code&gt;bun.lock&lt;/code&gt; are left alone. pnpm goes further: by default it re-checks every lockfile entry against the policy on install, which catches a lockfile someone generated under a weaker setting. You can turn that off with &lt;code&gt;trustLockfile: true&lt;/code&gt;, but only do it if every commit comes from people you trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing timestamps pass by default.&lt;/strong&gt; The gate relies on the registry's &lt;code&gt;time&lt;/code&gt; metadata. Some private registries and mirrors don't serve it, and both pnpm and Bun treat an undated version as passing. If you run an internal proxy, pnpm lets you tighten this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;minimumReleaseAgeIgnoreMissingTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;npm's config output can lie to you.&lt;/strong&gt; There's an open npm CLI issue where &lt;code&gt;npm config ls --long&lt;/code&gt; reports &lt;code&gt;min-release-age = null&lt;/code&gt; even though &lt;code&gt;npm install&lt;/code&gt; is honoring the setting, because npm converts it to a &lt;code&gt;--before&lt;/code&gt; date internally. Test the behavior with an install, not by reading the config dump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it bites, and how to live with it
&lt;/h2&gt;

&lt;p&gt;The most common complaint after pnpm 11 shipped wasn't about attacks. It was Dependabot. A bot opens a PR the moment a new version appears, CI runs a frozen install, and the brand-new version is younger than the cooldown, so CI goes red. The PR isn't wrong, it's just early. Your options are to delay the bot (Dependabot and Renovate both support a minimum age or cooldown on their side), or add a narrow, version-pinned exclusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;minimumReleaseAgeExclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nx@21.6.5&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webpack@4.47.0 || 5.102.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning to a specific version beats excluding a whole package name. A name-level exclusion means every future release of that package skips the gate, which is exactly the hole an attacker wants. pnpm can even clean up stale exclusions for you with &lt;code&gt;minimumReleaseAgeExcludePrune: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The other real cost is urgent security patches. When a CVE fix lands and you need it now, the cooldown is in your way. npm lets you override per command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;--min-release-age&lt;/span&gt; 0 some-package@1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use that deliberately, for the one package, and don't bake it into scripts.&lt;/p&gt;

&lt;p&gt;Finally, be honest about what this protects you from. A cooldown only helps against compromises that get caught quickly. A patient attacker who ships something that sits undetected for weeks walks right through it. It's one layer, alongside lockfiles, &lt;code&gt;allowBuilds&lt;/code&gt; / install-script controls, and provenance checks like pnpm's &lt;code&gt;trustPolicy: no-downgrade&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you're on pnpm 11 or newer, you've already got a soft version of this. Set &lt;code&gt;minimumReleaseAge&lt;/code&gt; explicitly so it becomes strict, add a cooldown to your update bot so it stops racing your own policy, and if you're on npm, add &lt;code&gt;min-release-age=1&lt;/code&gt; to your &lt;code&gt;.npmrc&lt;/code&gt; today. It's one line, and it turns "we installed the bad version" into "we read about the bad version."&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>CSS Container Style Queries: Let the Parent Set the Theme</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:10:57 +0000</pubDate>
      <link>https://dev.to/grimicorn/css-container-style-queries-let-the-parent-set-the-theme-k1n</link>
      <guid>https://dev.to/grimicorn/css-container-style-queries-let-the-parent-set-the-theme-k1n</guid>
      <description>&lt;p&gt;Every design system eventually grows a modifier class problem. The card needs a dark version for the footer, a compact version for the sidebar, and a "highlighted" version for the pricing page. So you add &lt;code&gt;.card--dark&lt;/code&gt;, &lt;code&gt;.card--compact&lt;/code&gt;, &lt;code&gt;.card--featured&lt;/code&gt;, and then you need a dark &lt;em&gt;compact&lt;/em&gt; card, and then the button inside the card also needs to know it's on a dark background. Now the class has to be threaded through three components, or you're writing selectors like &lt;code&gt;.card--dark .button&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Container size queries solved half of this in 2023: components could finally react to the space they're given instead of the viewport. The other half, reacting to the &lt;em&gt;context&lt;/em&gt; they're placed in, became Baseline Newly available in May 2026 when Firefox 151 shipped support for container style queries. Chrome and Edge have had them since 111, and Safari since 18, so this is ready for production with a sensible fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Style Queries Work
&lt;/h2&gt;

&lt;p&gt;A style query asks a question about the computed value of a custom property on an ancestor. If the answer is yes, the nested rules apply to descendants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.22&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt; &lt;span class="m"&gt;260&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.95&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are different from size queries. First, you don't need &lt;code&gt;container-type&lt;/code&gt;. Every element is a style container by default, so &lt;code&gt;@container style(...)&lt;/code&gt; checks the nearest ancestor with no setup at all. Second, only custom properties work today. The spec allows &lt;code&gt;style(font-weight: bold)&lt;/code&gt; on regular properties, but no browser implements that yet, so stick to &lt;code&gt;--anything&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have nested contexts (a light card inside a dark panel inside a light page), name the level you care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.site-footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;footer&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.newsletter-form&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a name, the query targets the nearest ancestor. With one, it targets the nearest ancestor with that &lt;code&gt;container-name&lt;/code&gt;, which is what you want when themes nest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gotcha: You Can't Query Yourself
&lt;/h2&gt;

&lt;p&gt;This is the mistake almost everyone makes on day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Does not work the way you'd expect */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"featured"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featured&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query is evaluated against the card's &lt;em&gt;ancestor&lt;/em&gt;, not the card. The card's parent never set &lt;code&gt;--variant&lt;/code&gt;, so the query fails. Custom properties inherit downward, not upward. The fix is to style a child of the element that holds the value, which in practice means having a wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--variant: featured"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card__inner"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Pro plan&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/signup"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start trial&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card__inner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;black&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need a single property on the element itself to change, that's what the &lt;code&gt;if()&lt;/code&gt; function is for. A useful rule of thumb: &lt;code&gt;if()&lt;/code&gt; produces a conditional value for one element, while a style query sets context for a whole subtree.&lt;/p&gt;

&lt;p&gt;It's also worth registering the property with &lt;code&gt;@property&lt;/code&gt;. A registered property has a known type and an initial value, so a container that never sets &lt;code&gt;--variant&lt;/code&gt; resolves to something predictable instead of the guaranteed-invalid value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--variant&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;custom-ident&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Range Queries and a Fallback Plan
&lt;/h2&gt;

&lt;p&gt;Exact matching covers themes and variants, but numbers are where it gets interesting. Chrome 142 added range syntax, so you can compare values instead of matching strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.meter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fill&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;90%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.meter__bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.6&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fill&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fill&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;90%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.meter__bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.75&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt; &lt;span class="m"&gt;85&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides of a comparison must resolve to the same numeric type (length, number, percentage, angle, time, frequency, or resolution). Note the split here: plain equality style queries are Baseline, but range syntax is Chromium-only for now. Treat range queries as a progressive enhancement.&lt;/p&gt;

&lt;p&gt;The fallback story is simple because style queries are additive. Write your default component styles outside any query, then layer the contextual variants inside &lt;code&gt;@container style()&lt;/code&gt; blocks. A browser that doesn't understand the at-rule skips it entirely and renders the default card. Nothing breaks; the footer card is just light instead of dark. If that's not acceptable for a given component, keep the old modifier class as a fallback alongside the style query and delete it once your analytics say the stragglers are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;Pick the one place in your codebase where a theme or variant class gets passed down through props or descendant selectors, and replace it with a single custom property on the wrapper and a style query for the children. You'll likely find that the components inside stop needing to know anything about where they live. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries" rel="noopener noreferrer"&gt;MDN guide to container size and style queries&lt;/a&gt; covers the full syntax, and Una Kravets' &lt;a href="https://una.im/range-style-queries" rel="noopener noreferrer"&gt;range style queries write-up&lt;/a&gt; is worth bookmarking for when the range syntax lands everywhere.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webapis</category>
      <category>containerqueries</category>
    </item>
    <item>
      <title>Map.getOrInsert: Stop Writing the has/get/set Dance</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:12:48 +0000</pubDate>
      <link>https://dev.to/grimicorn/mapgetorinsert-stop-writing-the-hasgetset-dance-bdd</link>
      <guid>https://dev.to/grimicorn/mapgetorinsert-stop-writing-the-hasgetset-dance-bdd</guid>
      <description>&lt;p&gt;Every JavaScript codebase has this pattern somewhere. Usually in a dozen places. You want to group things by a key, so you reach for a &lt;code&gt;Map&lt;/code&gt;, and then you write the same three lines you always write: check if the key exists, create an empty bucket if it doesn't, then fetch the bucket and push into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It's also three hash lookups for one logical operation, and in TypeScript the &lt;code&gt;get()&lt;/code&gt; call still returns &lt;code&gt;T | undefined&lt;/code&gt;, so you end up sprinkling a non-null assertion on a value you just guaranteed exists. The upsert proposal fixes this with two small methods, and as of 2026 they ship in every major browser engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  getOrInsert and getOrInsertComputed
&lt;/h2&gt;

&lt;p&gt;The proposal reached Stage 4 at TC39 in January 2026 and adds two methods to both &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;WeakMap&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;map.getOrInsert(key, defaultValue)&lt;/code&gt; returns the existing value for &lt;code&gt;key&lt;/code&gt;. If there isn't one, it stores &lt;code&gt;defaultValue&lt;/code&gt; and returns that.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;map.getOrInsertComputed(key, callback)&lt;/code&gt; does the same thing, but only calls &lt;code&gt;callback(key)&lt;/code&gt; to build the default when the key is actually missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The grouping loop above collapses to one line per item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting works the same way. Because the method returns the stored value, you can read and write in one expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wordCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\w&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;wordCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wordCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simple grouping you could also reach for &lt;code&gt;Map.groupBy()&lt;/code&gt;, which landed in 2024. The difference is that &lt;code&gt;groupBy&lt;/code&gt; builds a new map in one shot from an iterable, while &lt;code&gt;getOrInsert&lt;/code&gt; works on a map you're mutating over time: an event stream, a cache, or a registry that's filled lazily as requests come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use the Computed variant
&lt;/h2&gt;

&lt;p&gt;The catch with &lt;code&gt;getOrInsert&lt;/code&gt; is that its default is an ordinary argument, so it's evaluated every time, even when the key already exists. Passing &lt;code&gt;[]&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt; costs essentially nothing. Passing &lt;code&gt;new Set()&lt;/code&gt; allocates on every call. Passing &lt;code&gt;await fetchConfig(id)&lt;/code&gt; or &lt;code&gt;expensiveParse(source)&lt;/code&gt; is a real bug.&lt;/p&gt;

&lt;p&gt;MDN's own example shows it plainly: calling &lt;code&gt;map.getOrInsert("bar", defaultCreator("bar"))&lt;/code&gt; runs &lt;code&gt;defaultCreator&lt;/code&gt; even though &lt;code&gt;"bar"&lt;/code&gt; is already in the map, while &lt;code&gt;getOrInsertComputed("bar", defaultCreator)&lt;/code&gt; doesn't run it at all.&lt;/p&gt;

&lt;p&gt;So the rule of thumb is simple: use &lt;code&gt;getOrInsert&lt;/code&gt; for cheap literals, and &lt;code&gt;getOrInsertComputed&lt;/code&gt; whenever building the default allocates or does work. A memoization helper is the classic case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getHighlighter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;createHighlighter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github-dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback receives the key, so you can pass a named function instead of an inline closure. It must be callable: anything else throws a &lt;code&gt;TypeError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WeakMap is where it really shines
&lt;/h2&gt;

&lt;p&gt;The same two methods exist on &lt;code&gt;WeakMap&lt;/code&gt;, which makes per-object side data much cleaner. Say you need to attach state to DOM elements without putting properties on the nodes themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elementState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeakMap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;elementState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the element is removed and garbage collected, its entry goes with it. No cleanup code, no leaks, and no "is this initialized yet" branch anywhere in your handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you ship it today?
&lt;/h2&gt;

&lt;p&gt;Support is recent but broad. According to caniuse, &lt;code&gt;getOrInsertComputed&lt;/code&gt; is available in Chrome and Edge 145+, Firefox 144+, and Safari 26.2+ on both macOS and iOS, and MDN marks it Baseline 2026 (newly available). That still leaves a tail of users on older Safari and Samsung Internet, so for public sites you'll want a fallback for now. Both &lt;code&gt;core-js&lt;/code&gt; and es-shims provide polyfills, and the hand-rolled version is tiny if you'd rather avoid a dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrInsertComputed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callback must be callable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For server code, check your runtime before relying on it: &lt;code&gt;node -p "typeof Map.prototype.getOrInsert"&lt;/code&gt; prints &lt;code&gt;function&lt;/code&gt; when it's there. If your TypeScript &lt;code&gt;lib&lt;/code&gt; setting doesn't know about the methods yet, a small declaration file bridges the gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getOrInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;V&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;V&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;V&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;V&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that both return &lt;code&gt;V&lt;/code&gt;, not &lt;code&gt;V | undefined&lt;/code&gt;, which is the other quiet win here: the non-null assertions go away along with the boilerplate.&lt;/p&gt;

&lt;p&gt;The upsert methods aren't flashy, but they're the kind of feature you end up using every day once you know it exists. Next time you type &lt;code&gt;if (!map.has(key))&lt;/code&gt;, stop and reach for &lt;code&gt;getOrInsert&lt;/code&gt; instead. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsertComputed" rel="noopener noreferrer"&gt;MDN reference for getOrInsertComputed&lt;/a&gt; is a good place to start.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>Astro Live Content Collections: Fresh CMS Data Without a Rebuild</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:12:45 +0000</pubDate>
      <link>https://dev.to/grimicorn/astro-live-content-collections-fresh-cms-data-without-a-rebuild-4p6a</link>
      <guid>https://dev.to/grimicorn/astro-live-content-collections-fresh-cms-data-without-a-rebuild-4p6a</guid>
      <description>&lt;p&gt;Every Jamstack site eventually hits the same wall. The marketing team fixes a typo in the CMS, hits publish, and then asks why the site still shows the old copy. The honest answer is "because the site is a pile of HTML that was generated twenty minutes ago, and the rebuild is still in the queue." Webhooks, incremental builds, and on-demand revalidation all exist to paper over that gap, and each one adds another moving part you have to babysit.&lt;/p&gt;

&lt;p&gt;Astro's answer is live content collections. They shipped as stable in Astro 6 and picked up first-class caching in Astro 7. The pitch is simple: keep the content collection API you already know, but fetch the data when the page is requested instead of when the site is built. No rebuild, no webhook, and the same typed &lt;code&gt;entry.data&lt;/code&gt; you get from build-time collections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build-time vs. live: two configs, one mental model
&lt;/h2&gt;

&lt;p&gt;Build-time collections live in &lt;code&gt;src/content.config.ts&lt;/code&gt;, run their loaders during &lt;code&gt;astro build&lt;/code&gt;, and persist the results in Astro's data store. That is still the right default. It is fast, works with MDX, and lets Astro optimize images.&lt;/p&gt;

&lt;p&gt;Live collections live in a separate file, &lt;code&gt;src/live.config.ts&lt;/code&gt;, and use &lt;code&gt;defineLiveCollection()&lt;/code&gt;. They require an adapter for on-demand rendering, because the whole point is that the data is fetched per request. There are no built-in live loaders, so you write one or install a community loader.&lt;/p&gt;

&lt;p&gt;A live loader is an object with a &lt;code&gt;name&lt;/code&gt; and two methods: &lt;code&gt;loadCollection()&lt;/code&gt; for lists and &lt;code&gt;loadEntry()&lt;/code&gt; for a single item. Each returns data or an &lt;code&gt;Error&lt;/code&gt;, never throws.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/loaders/changelog.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LiveLoader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/loaders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Release&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;changelogLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;LiveLoader&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;changelog-loader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;loadCollection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`?tag=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/releases&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;releases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;releases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
          &lt;span class="na"&gt;cacheHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;releases&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load releases&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;loadEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/releases/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Release not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Release&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;cacheHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`release-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load release&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register it. The Zod schema is optional, but if you provide one it wins over the loader's generic types and validation errors come back as a typed error instead of a crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/live.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineLiveCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;changelogLoader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./loaders/changelog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;releases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineLiveCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;changelogLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CMS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CMS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;releases&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Querying live data with explicit errors
&lt;/h2&gt;

&lt;p&gt;On the page side you swap &lt;code&gt;getEntry()&lt;/code&gt; for &lt;code&gt;getLiveEntry()&lt;/code&gt; and &lt;code&gt;getCollection()&lt;/code&gt; for &lt;code&gt;getLiveCollection()&lt;/code&gt;. The difference that matters is the return shape: you get &lt;code&gt;{ entry, error }&lt;/code&gt; back rather than an exception, which forces you to decide what a CMS outage looks like for your users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/changelog/[id].astro
export const prerender = false;
import { getLiveEntry, render } from 'astro:content';
import { LiveEntryNotFoundError } from 'astro/content/runtime';

const { entry, error } = await getLiveEntry('releases', Astro.params.id);

if (error) {
  if (error instanceof LiveEntryNotFoundError) return Astro.rewrite('/404');
  return new Response('Changelog temporarily unavailable', { status: 503 });
}

Astro.cache.set(entry);
Astro.cache.set({ maxAge: 300, swr: 60 });

const { Content } = await render(entry);
---
&amp;lt;h1&amp;gt;{entry.data.title}&amp;lt;/h1&amp;gt;
&amp;lt;Content /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error union is narrow: &lt;code&gt;LiveEntryNotFoundError&lt;/code&gt;, &lt;code&gt;LiveCollectionValidationError&lt;/code&gt;, &lt;code&gt;LiveCollectionCacheHintError&lt;/code&gt;, or a generic &lt;code&gt;LiveCollectionError&lt;/code&gt;. Because the loader returned a &lt;code&gt;rendered.html&lt;/code&gt; property, &lt;code&gt;render()&lt;/code&gt; and &lt;code&gt;&amp;lt;Content /&amp;gt;&lt;/code&gt; work exactly like they do for Markdown collections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching is what makes this not-a-regression
&lt;/h2&gt;

&lt;p&gt;Fetching from a CMS on every request is slower than serving static HTML, and the Astro docs say so plainly. That is where Astro 7's route caching comes in. Configure a provider once (the in-memory one for Node, or the experimental CDN providers for Netlify, Vercel, and Cloudflare) and pass the live entry to &lt;code&gt;Astro.cache.set()&lt;/code&gt;. Astro pulls the loader's cache hint off it, tagging the response and setting &lt;code&gt;Last-Modified&lt;/code&gt; for you. Multiple &lt;code&gt;set()&lt;/code&gt; calls merge: tags accumulate, &lt;code&gt;lastModified&lt;/code&gt; takes the newest date, and &lt;code&gt;maxAge&lt;/code&gt; is last-write-wins.&lt;/p&gt;

&lt;p&gt;Invalidation then becomes a tiny endpoint your CMS webhook can hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/revalidate.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`release-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;purged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the inversion. In the classic Jamstack setup, the webhook triggers a full rebuild. Here it purges one cache tag, and the next request fetches fresh data.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stay on build-time collections
&lt;/h2&gt;

&lt;p&gt;Live collections give up a few things: no MDX rendering at runtime, no image optimization, and no persistence in the content layer data store. If your content changes a few times a week, build-time collections plus a deploy hook are still simpler and faster. Reach for live collections when freshness is the feature: inventory, pricing, changelogs, CMS draft previews, or filters driven by request parameters.&lt;/p&gt;

&lt;p&gt;The nice part is you don't have to pick one for the whole site. Both kinds coexist, so your docs can stay static while the one page that always goes stale finally doesn't. Start with the &lt;a href="https://docs.astro.build/en/guides/content-collections/#live-content-collections" rel="noopener noreferrer"&gt;content collections guide&lt;/a&gt; and the &lt;a href="https://docs.astro.build/en/guides/caching/" rel="noopener noreferrer"&gt;route caching docs&lt;/a&gt;, and try converting your most-complained-about page first.&lt;/p&gt;

</description>
      <category>jamstack</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Nuxt createUseFetch: One Typed API Client Without the Wrapper Boilerplate</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 05 Oct 2026 10:12:38 +0000</pubDate>
      <link>https://dev.to/grimicorn/nuxt-createusefetch-one-typed-api-client-without-the-wrapper-boilerplate-4mj0</link>
      <guid>https://dev.to/grimicorn/nuxt-createusefetch-one-typed-api-client-without-the-wrapper-boilerplate-4mj0</guid>
      <description>&lt;p&gt;Every Nuxt app that talks to a real backend ends up with the same problem. You call &lt;code&gt;useFetch('/users')&lt;/code&gt; in one page, then &lt;code&gt;useFetch('/orders')&lt;/code&gt; in another, and before long every call site is repeating the same &lt;code&gt;baseURL&lt;/code&gt;, the same &lt;code&gt;Authorization&lt;/code&gt; header, and the same "redirect to login on 401" logic. So you write a wrapper. Then you discover the wrapper loses some of &lt;code&gt;useFetch&lt;/code&gt;'s typing, or its generics get ugly, or you are not sure it still plays nicely with SSR deduplication.&lt;/p&gt;

&lt;p&gt;Nuxt 4.4 shipped a first-party answer: &lt;code&gt;createUseFetch&lt;/code&gt; and &lt;code&gt;createUseAsyncData&lt;/code&gt;. They are factories that return a composable with the exact same signature as &lt;code&gt;useFetch&lt;/code&gt; (or &lt;code&gt;useAsyncData&lt;/code&gt;), just with your defaults baked in. No hand-rolled generics, no guessing about keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic factory
&lt;/h2&gt;

&lt;p&gt;The factory lives in your &lt;code&gt;composables/&lt;/code&gt; directory like any other composable. Here is an API client for an external backend, adapted from the recipe in the Nuxt docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/composables/useAPI.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUseFetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUserSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onResponseError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;navigateTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now pages just ask for data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useAPI&lt;/code&gt; returns the same &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;refresh&lt;/code&gt; you already know, with the same typing. Every option &lt;code&gt;useFetch&lt;/code&gt; accepts (&lt;code&gt;query&lt;/code&gt;, &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;pick&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;getCachedData&lt;/code&gt;, and so on) is still available at the call site.&lt;/p&gt;

&lt;p&gt;One rule to remember: &lt;code&gt;createUseFetch&lt;/code&gt; is a &lt;strong&gt;compiler macro&lt;/strong&gt;. It has to be an exported declaration in &lt;code&gt;composables/&lt;/code&gt; (or another directory Nuxt scans). That is how Nuxt finds it at build time and injects the de-duplication keys that make SSR hydration reuse the server payload instead of fetching twice. Define it inside a component or a random utility file and you lose that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defaults vs. overrides
&lt;/h2&gt;

&lt;p&gt;The factory has two modes, and picking the right one matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass a plain object&lt;/strong&gt; and your options are &lt;em&gt;defaults&lt;/em&gt;. The caller wins on any conflict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUseFetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Caller overrides baseURL for this one call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://status.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pass a function&lt;/strong&gt; and your options &lt;em&gt;override&lt;/em&gt; the caller's. The function receives what the caller passed, so you decide how to merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The base URL is enforced; callers cannot point this client elsewhere&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useBillingAPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUseFetch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;callerOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;callerOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billingApiUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function form is also the one to reach for whenever your defaults need something from the Nuxt context, like &lt;code&gt;useRuntimeConfig()&lt;/code&gt; or &lt;code&gt;useNuxtApp()&lt;/code&gt;. The function runs at the call site, inside setup, where a Nuxt instance exists. A plain object is evaluated once at module scope, where it does not.&lt;/p&gt;

&lt;p&gt;A practical way to think about it: object mode for convenience ("most calls want this"), function mode for policy ("every call must have this").&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging in a custom &lt;code&gt;$fetch&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you already have a configured &lt;code&gt;ofetch&lt;/code&gt; instance from a plugin, perhaps with retry logic or logging, you can hand it to the factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/plugins/api.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtPlugin&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retryStatusCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/composables/useAPI.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUseFetch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;callerOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;useNuxtApp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;$api&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;callerOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the function form again: &lt;code&gt;useNuxtApp()&lt;/code&gt; has to run in setup, so it cannot sit in a plain object at the top of the file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createUseAsyncData&lt;/code&gt; follows the same pattern for cases where the "fetch" is not an HTTP call at all, such as a GraphQL client, an SDK, or a Supabase query, but you still want shared defaults like &lt;code&gt;lazy&lt;/code&gt;, &lt;code&gt;deep&lt;/code&gt;, or &lt;code&gt;getCachedData&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to bother
&lt;/h2&gt;

&lt;p&gt;If your app hits one same-origin &lt;code&gt;/api&lt;/code&gt; route and needs no headers, plain &lt;code&gt;useFetch&lt;/code&gt; is fine; a factory adds a file and a name for no gain. And remember that &lt;code&gt;useFetch&lt;/code&gt;-style composables are for loading data during render. For a form submit or a button click, call &lt;code&gt;$fetch&lt;/code&gt; (or your plugin's &lt;code&gt;$api&lt;/code&gt;) directly. Wrapping event handlers in &lt;code&gt;useAPI&lt;/code&gt; will not give you SSR benefits and can cause confusing caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;createUseFetch&lt;/code&gt; turns a pattern almost every Nuxt team reinvented into one line that keeps full typing and SSR key handling intact. If you are on Nuxt 4.4 or later, look for your existing &lt;code&gt;useFetch&lt;/code&gt; wrappers, move them into &lt;code&gt;composables/&lt;/code&gt; as factories, and choose object or function mode deliberately. The &lt;a href="https://nuxt.com/docs/4.x/api/composables/create-use-fetch" rel="noopener noreferrer"&gt;createUseFetch docs&lt;/a&gt; and the &lt;a href="https://nuxt.com/docs/4.x/guide/recipes/custom-usefetch" rel="noopener noreferrer"&gt;custom useFetch recipe&lt;/a&gt; cover the remaining options.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>typescript</category>
      <category>webapis</category>
    </item>
    <item>
      <title>Your tsconfig Is Still Written for TypeScript 5. Here's the Cleanup TS 7 Forces</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:12:42 +0000</pubDate>
      <link>https://dev.to/grimicorn/your-tsconfig-is-still-written-for-typescript-5-heres-the-cleanup-ts-7-forces-4m6e</link>
      <guid>https://dev.to/grimicorn/your-tsconfig-is-still-written-for-typescript-5-heres-the-cleanup-ts-7-forces-4m6e</guid>
      <description>&lt;p&gt;Most &lt;code&gt;tsconfig.json&lt;/code&gt; files are archaeology. Somebody copied one from a starter repo in 2021, somebody else added &lt;code&gt;baseUrl&lt;/code&gt; so the &lt;code&gt;@/&lt;/code&gt; imports would work, and a third person set &lt;code&gt;moduleResolution: "node"&lt;/code&gt; because a Stack Overflow answer said so. Nobody has touched it since, because it worked.&lt;/p&gt;

&lt;p&gt;That stops being true this year. TypeScript 6.0 (March 2026) was a deliberate bridge release: it changed a batch of defaults and deprecated a list of legacy options. TypeScript 7.0, the Go-native compiler that went GA in July, keeps the new defaults and &lt;strong&gt;removes&lt;/strong&gt; the deprecated options outright. If you upgraded to 6.0 and silenced the warnings with &lt;code&gt;"ignoreDeprecations": "6.0"&lt;/code&gt;, you didn't fix anything. You scheduled the breakage for your TS 7 upgrade.&lt;/p&gt;

&lt;p&gt;Here's the audit, in the order it will actually bite you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two changes with confusing error messages
&lt;/h2&gt;

&lt;p&gt;The TypeScript team called these out up front because the errors they produce don't point at the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;types&lt;/code&gt; now defaults to &lt;code&gt;[]&lt;/code&gt;.&lt;/strong&gt; Previously, TypeScript loaded every package under &lt;code&gt;node_modules/@types&lt;/code&gt; into the global scope. That's how &lt;code&gt;process&lt;/code&gt;, &lt;code&gt;describe&lt;/code&gt;, and &lt;code&gt;it&lt;/code&gt; showed up without an import. In 6.0 and later, nothing is loaded unless you list it. The symptom is a flood of &lt;code&gt;Cannot find name 'process'&lt;/code&gt; or &lt;code&gt;Cannot find name 'describe'&lt;/code&gt; errors that look like a broken install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest/globals"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can restore the old behavior with &lt;code&gt;"types": ["*"]&lt;/code&gt;, but don't. The team reports that many projects saw 20 to 50 percent faster builds just from listing the types they actually use, because a typical monorepo transitively drags in hundreds of &lt;code&gt;@types&lt;/code&gt; packages nobody imports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;rootDir&lt;/code&gt; now defaults to the tsconfig's directory.&lt;/strong&gt; It used to be inferred from the common ancestor of your source files. If your sources live in &lt;code&gt;src/&lt;/code&gt; and you never set &lt;code&gt;rootDir&lt;/code&gt;, your output silently moves from &lt;code&gt;dist/index.js&lt;/code&gt; to &lt;code&gt;dist/src/index.js&lt;/code&gt;. Nothing errors. Your &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;main&lt;/code&gt; field just points at a file that no longer exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use a bundler and &lt;code&gt;noEmit&lt;/code&gt;, this one won't affect you. If you publish a library with &lt;code&gt;tsc&lt;/code&gt;, check it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defaults that flipped quietly
&lt;/h2&gt;

&lt;p&gt;These changed in 6.0 and are the baseline in 7.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strict&lt;/code&gt; is now &lt;code&gt;true&lt;/code&gt;. If you were relying on the old &lt;code&gt;false&lt;/code&gt; default, you'll suddenly get every &lt;code&gt;strictNullChecks&lt;/code&gt; and &lt;code&gt;noImplicitAny&lt;/code&gt; error at once.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;module&lt;/code&gt; defaults to &lt;code&gt;esnext&lt;/code&gt;, and &lt;code&gt;target&lt;/code&gt; floats to the newest supported spec (currently &lt;code&gt;es2025&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noUncheckedSideEffectImports&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, so a typo in &lt;code&gt;import './polyfil'&lt;/code&gt; is now an error instead of a silent no-op.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest move with &lt;code&gt;strict&lt;/code&gt; is to set it explicitly either way. If your codebase isn't ready, write &lt;code&gt;"strict": false&lt;/code&gt; so the decision is visible in the file rather than inherited from a default that changed under you. Then turn on the individual checks one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options that are gone in 7.0
&lt;/h2&gt;

&lt;p&gt;These were deprecated in 6.0 and do not exist in 7.0:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Replace with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;moduleResolution: "node"&lt;/code&gt; / &lt;code&gt;"node10"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"nodenext"&lt;/code&gt; for Node, &lt;code&gt;"bundler"&lt;/code&gt; for Vite/webpack/Bun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;moduleResolution: "classic"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"bundler"&lt;/code&gt; or &lt;code&gt;"nodenext"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;baseUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;explicit prefixes in &lt;code&gt;paths&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;target: "es5"&lt;/code&gt;, &lt;code&gt;downlevelIteration&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;es2015&lt;/code&gt; or later; let your bundler handle older targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;module: "amd"&lt;/code&gt;, &lt;code&gt;"umd"&lt;/code&gt;, &lt;code&gt;"system"&lt;/code&gt;, &lt;code&gt;outFile&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a bundler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;esModuleInterop: false&lt;/code&gt;, &lt;code&gt;allowSyntheticDefaultImports: false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;delete them; both are always on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;baseUrl&lt;/code&gt; is the one most frontend projects hit. Almost everyone used it as a prefix for &lt;code&gt;paths&lt;/code&gt;, but it was also a lookup root, which meant TypeScript would happily resolve &lt;code&gt;import x from "utils"&lt;/code&gt; to &lt;code&gt;src/utils.ts&lt;/code&gt; even though no bundler would. The fix is mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  {
    "compilerOptions": {
&lt;span class="gd"&gt;-     "baseUrl": "./src",
&lt;/span&gt;      "paths": {
&lt;span class="gd"&gt;-       "@/*": ["*"]
&lt;/span&gt;&lt;span class="gi"&gt;+       "@/*": ["./src/*"]
&lt;/span&gt;      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One related improvement: 6.0 now allows &lt;code&gt;moduleResolution: "bundler"&lt;/code&gt; together with &lt;code&gt;module: "commonjs"&lt;/code&gt;, which is the cleanest landing spot for older projects that still emit CommonJS but used &lt;code&gt;node10&lt;/code&gt; resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing the migration without guessing
&lt;/h2&gt;

&lt;p&gt;Run your current version with the deprecation warnings visible first. Remove &lt;code&gt;ignoreDeprecations&lt;/code&gt; if you added it, run &lt;code&gt;tsc --noEmit&lt;/code&gt;, and treat every warning as a ticket. For &lt;code&gt;baseUrl&lt;/code&gt; and &lt;code&gt;rootDir&lt;/code&gt;, the TypeScript team's experimental &lt;a href="https://github.com/andrewbranch/ts5to6" rel="noopener noreferrer"&gt;&lt;code&gt;ts5to6&lt;/code&gt;&lt;/a&gt; codemod will rewrite the config across a monorepo for you.&lt;/p&gt;

&lt;p&gt;Once 6.0 is clean, 7.0 should be uneventful for the config side. If you still see odd inference differences, 6.0's &lt;code&gt;--stableTypeOrdering&lt;/code&gt; flag makes the JS compiler order types the way the Go compiler does, so you can shake those out before switching binaries.&lt;/p&gt;

&lt;p&gt;A tsconfig is supposed to describe what your project actually does. After this cleanup, it finally will: explicit &lt;code&gt;types&lt;/code&gt;, explicit &lt;code&gt;rootDir&lt;/code&gt;, a &lt;code&gt;moduleResolution&lt;/code&gt; that matches your real runtime, and no lookup roots that resolve imports your bundler never could. The full list of changes is in the &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/" rel="noopener noreferrer"&gt;TypeScript 6.0 release notes&lt;/a&gt;, and it's worth one careful read before you bump the version in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>tooling</category>
      <category>bundlers</category>
      <category>node</category>
    </item>
    <item>
      <title>Node.js Type Stripping: Run TypeScript Without a Build Step</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:12:31 +0000</pubDate>
      <link>https://dev.to/grimicorn/nodejs-type-stripping-run-typescript-without-a-build-step-29if</link>
      <guid>https://dev.to/grimicorn/nodejs-type-stripping-run-typescript-without-a-build-step-29if</guid>
      <description>&lt;p&gt;Every Node project written in TypeScript used to carry the same small tax. You wanted to run a one-off script, so you reached for &lt;code&gt;ts-node&lt;/code&gt;, then fought its ESM loader, then switched to &lt;code&gt;tsx&lt;/code&gt;, then added a &lt;code&gt;build&lt;/code&gt; step so production wouldn't depend on either. None of that was hard. It was just friction, repeated in every repo.&lt;/p&gt;

&lt;p&gt;That tax is mostly gone. Type stripping has been on by default since Node 23.6 and 22.18, and as of Node 25.2 and 24.12 the docs mark it &lt;strong&gt;Stable&lt;/strong&gt;. You can type &lt;code&gt;node script.ts&lt;/code&gt; and it runs. The catch is that "Node runs TypeScript" is a slightly misleading headline. Node doesn't compile TypeScript. It erases it, and knowing the difference is what keeps you from hitting confusing runtime errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erasing, not compiling
&lt;/h2&gt;

&lt;p&gt;When Node loads a &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.mts&lt;/code&gt;, or &lt;code&gt;.cts&lt;/code&gt; file, it hands the source to a small SWC-based module called amaro. Amaro finds the type annotations, interfaces, and type aliases and replaces them with whitespace. What's left is plain JavaScript with the exact same line and column positions, so stack traces point at the right place without source maps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// greet.ts&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Welcome back, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; visits)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node greet.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things this does &lt;strong&gt;not&lt;/strong&gt; do. It doesn't type check: a file full of type errors runs fine as long as the JavaScript underneath is valid. And it doesn't read &lt;code&gt;tsconfig.json&lt;/code&gt; at all, so &lt;code&gt;paths&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt; downleveling, and every other compiler setting are ignored. Your editor and &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI are still where type safety lives. Node just stopped making you transpile before you can execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The syntax that breaks
&lt;/h2&gt;

&lt;p&gt;Because Node only deletes code, anything in TypeScript that &lt;em&gt;generates&lt;/em&gt; JavaScript can't work. The big four are &lt;code&gt;enum&lt;/code&gt; declarations, &lt;code&gt;namespace&lt;/code&gt; blocks with runtime values, constructor parameter properties, and import aliases like &lt;code&gt;import Alias = Some.Namespace&lt;/code&gt;. Hit one and Node throws &lt;code&gt;ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fails: parameter properties generate assignments&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Works: the same thing, written as erasable syntax&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enums are the one most teams trip over. A &lt;code&gt;const&lt;/code&gt; object with &lt;code&gt;as const&lt;/code&gt; gives you the same autocomplete and a real runtime value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There used to be an escape hatch, &lt;code&gt;--experimental-transform-types&lt;/code&gt;, that handled enums and friends. Node 26 removed it. If you need full TypeScript semantics at runtime, the docs now point you to a third-party loader like &lt;code&gt;tsx&lt;/code&gt;. For everything else, write erasable TypeScript.&lt;/p&gt;

&lt;p&gt;Decorators and &lt;code&gt;.tsx&lt;/code&gt; files are also out. Decorators wait on native JavaScript support, and JSX needs a real transform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the compiler enforce it for you
&lt;/h2&gt;

&lt;p&gt;You don't have to memorize that list. TypeScript 5.8 added &lt;code&gt;erasableSyntaxOnly&lt;/code&gt;, which turns every non-erasable construct into a compile error. The Node docs recommend this setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esnext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rewriteRelativeImportExtensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"erasableSyntaxOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verbatimModuleSyntax&lt;/code&gt; matters more than it looks. Node can only strip a type import it can recognize, so you must write &lt;code&gt;import type { User } from './user.ts'&lt;/code&gt; or &lt;code&gt;import { greet, type User } from './greet.ts'&lt;/code&gt;. A plain &lt;code&gt;import { User }&lt;/code&gt; is treated as a value import and fails at runtime because nothing named &lt;code&gt;User&lt;/code&gt; exists in the emitted JavaScript. This flag makes &lt;code&gt;tsc&lt;/code&gt; catch that before Node does.&lt;/p&gt;

&lt;p&gt;Two more habits follow from "Node treats &lt;code&gt;.ts&lt;/code&gt; like &lt;code&gt;.js&lt;/code&gt;." Import specifiers need the real extension (&lt;code&gt;./db.ts&lt;/code&gt;, not &lt;code&gt;./db&lt;/code&gt;), and module format follows the nearest &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;"type"&lt;/code&gt; field, just like JavaScript. &lt;code&gt;rewriteRelativeImportExtensions&lt;/code&gt; lets you keep writing &lt;code&gt;.ts&lt;/code&gt; specifiers and still emit working &lt;code&gt;.js&lt;/code&gt; if you later publish compiled output.&lt;/p&gt;

&lt;p&gt;If you relied on &lt;code&gt;tsconfig&lt;/code&gt; &lt;code&gt;paths&lt;/code&gt; for aliases like &lt;code&gt;@/lib/db&lt;/code&gt;, switch to Node's own subpath imports. They're defined in &lt;code&gt;package.json&lt;/code&gt; and must start with &lt;code&gt;#&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"#lib/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/lib/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#lib/db.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where it fits, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Type stripping shines for scripts, CLIs, internal services, and test files. Pair it with the built-in test runner and a small project can drop &lt;code&gt;ts-node&lt;/code&gt;, &lt;code&gt;tsx&lt;/code&gt;, and a build step entirely.&lt;/p&gt;

&lt;p&gt;It's not for publishing libraries. Node deliberately refuses to strip types from anything under &lt;code&gt;node_modules&lt;/code&gt;, so packages still need to ship compiled JavaScript and &lt;code&gt;.d.ts&lt;/code&gt; files. And if your codebase leans on enums, decorators, or JSX on the server, a full transpiler remains the right tool.&lt;/p&gt;

&lt;p&gt;The practical move this week: add &lt;code&gt;erasableSyntaxOnly&lt;/code&gt; to an existing project and see what lights up. If the list is short, you're a few small refactors away from deleting a dependency and a build step. The &lt;a href="https://nodejs.org/api/typescript.html" rel="noopener noreferrer"&gt;Node.js TypeScript docs&lt;/a&gt; cover the remaining edge cases.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Compression Dictionaries: Ship Only the Diff When Your Bundle Changes</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 28 Sep 2026 10:12:46 +0000</pubDate>
      <link>https://dev.to/grimicorn/compression-dictionaries-ship-only-the-diff-when-your-bundle-changes-2dmf</link>
      <guid>https://dev.to/grimicorn/compression-dictionaries-ship-only-the-diff-when-your-bundle-changes-2dmf</guid>
      <description>&lt;p&gt;You ship a one-line bug fix. Your bundler dutifully produces &lt;code&gt;app.8f3c1a.js&lt;/code&gt;, a brand new filename, and every returning visitor downloads the whole 270 KB file again. The browser already has 99% of those bytes sitting in its cache under the old hash, but HTTP compression has no way to say "you already know most of this." Gzip and Brotli compress each response in isolation, so a tiny code change costs a full re-download.&lt;/p&gt;

&lt;p&gt;Compression Dictionary Transport fixes exactly that. It became a real standard as &lt;a href="https://datatracker.ietf.org/doc/rfc9842/" rel="noopener noreferrer"&gt;RFC 9842&lt;/a&gt; in September 2025, it ships in Chrome and Edge 130+, and Cloudflare opened a beta for it in April 2026. The idea is simple: let the browser tell the server which previous response it has cached, and let the server compress the new response against it. For versioned JavaScript, the result is often a delta measured in single-digit kilobytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the handshake works
&lt;/h2&gt;

&lt;p&gt;There are three moving parts: a response that volunteers to be a dictionary, a request that advertises which dictionary is available, and a response compressed with it.&lt;/p&gt;

&lt;p&gt;First, the server marks a resource as reusable with the &lt;code&gt;Use-As-Dictionary&lt;/code&gt; header. The &lt;code&gt;match&lt;/code&gt; value is a URL pattern describing which future requests this file can help with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/javascript&lt;/span&gt;
&lt;span class="na"&gt;Cache-Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public, max-age=31536000&lt;/span&gt;
&lt;span class="na"&gt;Use-As-Dictionary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;match="/js/app.*.js", id="app-v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser stores that response as a dictionary. The next time it requests something matching &lt;code&gt;/js/app.*.js&lt;/code&gt;, say after you deploy &lt;code&gt;app.2d9e44.js&lt;/code&gt;, it adds two things to the request: new encodings in &lt;code&gt;Accept-Encoding&lt;/code&gt;, and the SHA-256 hash of the dictionary it holds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/js/app.2d9e44.js&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gzip, br, zstd, dcb, dcz&lt;/span&gt;
&lt;span class="na"&gt;Available-Dictionary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;:pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:&lt;/span&gt;
&lt;span class="na"&gt;Dictionary-ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"app-v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dcb&lt;/code&gt; is dictionary-compressed Brotli and &lt;code&gt;dcz&lt;/code&gt; is dictionary-compressed Zstandard. If the server has a delta prepared for that exact hash, it responds with one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dcz&lt;/span&gt;
&lt;span class="na"&gt;Vary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accept-encoding, available-dictionary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Vary&lt;/code&gt; header is not optional. Without it, a CDN could happily serve a dictionary-compressed body to a client that never had the dictionary, and that client gets garbage. Also note that &lt;code&gt;Dictionary-ID&lt;/code&gt; is a convenience label; the server must still verify the &lt;code&gt;Available-Dictionary&lt;/code&gt; hash before trusting that the client has the bytes it thinks it has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the deltas at deploy time
&lt;/h2&gt;

&lt;p&gt;The practical pattern for static assets is to precompress at build time. For every new bundle, compress it against the previous release's version of the same file and store the result next to it. The Zstandard and Brotli CLIs both accept a dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# previous release is the dictionary, new build is the payload&lt;/span&gt;
zstd &lt;span class="nt"&gt;-19&lt;/span&gt; &lt;span class="nt"&gt;--patch-from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist-prev/app.8f3c1a.js dist/app.2d9e44.js &lt;span class="nt"&gt;-o&lt;/span&gt; dist/app.2d9e44.js.dcz-raw

&lt;span class="c"&gt;# the hash the browser will send in Available-Dictionary&lt;/span&gt;
openssl dgst &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-binary&lt;/span&gt; dist-prev/app.8f3c1a.js | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dcz&lt;/code&gt; and &lt;code&gt;dcb&lt;/code&gt; formats wrap the compressed stream with a short header that embeds the dictionary hash, so the browser can confirm it decoded against the right file. Tooling like Cloudflare's implementation handles that framing for you; if you roll your own, follow the RFC's format section rather than guessing.&lt;/p&gt;

&lt;p&gt;On the server, the logic is a lookup: does a delta exist for this path and this dictionary hash? If yes, serve it. If no, fall back to normal Brotli.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/js/:file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;available-dictionary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// strip the colons&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accepts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept-encoding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;deltaIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;vary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Available-Dictionary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use-As-Dictionary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;match="/js/app.*.js", id="app"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;accepts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dcz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Encoding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dcz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// regular static handler serves br/gzip&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the new file also sends &lt;code&gt;Use-As-Dictionary&lt;/code&gt;. Each release becomes the dictionary for the next one, so a returning visitor keeps getting deltas across deploys instead of just one.&lt;/p&gt;

&lt;p&gt;How big is the win? In Cloudflare's lab test, a 272 KB JavaScript asset was 92.2 KB with gzip. Compressed against its previous version with &lt;code&gt;dcz&lt;/code&gt;, it came out at 2.6 KB. Your numbers depend entirely on how much actually changed between builds, but for frequent small deploys that ratio is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared dictionaries for first visits
&lt;/h2&gt;

&lt;p&gt;Deltas only help returning visitors. For first loads, you can ship a separate dictionary built from content common across your pages, like your HTML boilerplate or shared component markup, and hint it to the browser at idle priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"compression-dictionary"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/dictionary.dat"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with a response header: &lt;code&gt;Link: &amp;lt;/dictionary.dat&amp;gt;; rel="compression-dictionary"&lt;/code&gt;. The browser downloads it in the background, and later page navigations can be compressed against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fine print
&lt;/h2&gt;

&lt;p&gt;A few rules will bite you if you skip them. Dictionaries must be same-origin with the resources they compress, and dictionary-compressed responses must be same-origin with the document or served with proper CORS. If you use &lt;code&gt;&amp;lt;link rel="compression-dictionary"&amp;gt;&lt;/code&gt; with a Content Security Policy, &lt;code&gt;connect-src&lt;/code&gt; has to allow the dictionary URL. Only secure contexts participate.&lt;/p&gt;

&lt;p&gt;Browser support is the bigger caveat. Chromium browsers support it today, Firefox has an active implementation bug, and Safari has not announced support. That is fine, because the design degrades gracefully: browsers that don't send &lt;code&gt;dcb&lt;/code&gt; or &lt;code&gt;dcz&lt;/code&gt; in &lt;code&gt;Accept-Encoding&lt;/code&gt; never see a dictionary response and just get your normal Brotli. You are adding a fast path, not a dependency.&lt;/p&gt;

&lt;p&gt;If you deploy often and your users come back often, this is one of the rare performance wins that gets bigger the more you ship. Start with your largest versioned JavaScript bundle, precompress one delta per release, and measure transfer size in DevTools on a repeat visit. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Compression_dictionary_transport" rel="noopener noreferrer"&gt;MDN guide to compression dictionary transport&lt;/a&gt; and the &lt;a href="https://developer.chrome.com/blog/shared-dictionary-compression" rel="noopener noreferrer"&gt;Chrome team's write-up&lt;/a&gt; cover the remaining details.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>performance</category>
      <category>webapis</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Uint8Array toBase64 and toHex: Stop Round-Tripping Bytes Through btoa</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:13:48 +0000</pubDate>
      <link>https://dev.to/grimicorn/uint8array-tobase64-and-tohex-stop-round-tripping-bytes-through-btoa-193l</link>
      <guid>https://dev.to/grimicorn/uint8array-tobase64-and-tohex-stop-round-tripping-bytes-through-btoa-193l</guid>
      <description>&lt;p&gt;Every codebase that touches binary data has the same little helper buried in a &lt;code&gt;utils&lt;/code&gt; folder. It takes a &lt;code&gt;Uint8Array&lt;/code&gt;, spreads it into &lt;code&gt;String.fromCharCode&lt;/code&gt;, hands the result to &lt;code&gt;btoa&lt;/code&gt;, and hopes for the best. Its sibling goes the other way with &lt;code&gt;atob&lt;/code&gt; and a &lt;code&gt;charCodeAt&lt;/code&gt; loop. And somewhere nearby there's a third helper that maps each byte to &lt;code&gt;toString(16).padStart(2, '0')&lt;/code&gt; so you can print a hash.&lt;/p&gt;

&lt;p&gt;None of those helpers are wrong, exactly. They're just working around the fact that &lt;code&gt;btoa&lt;/code&gt; and &lt;code&gt;atob&lt;/code&gt; were designed for strings, not bytes. That gap is now closed: &lt;code&gt;Uint8Array&lt;/code&gt; has native methods for base64 and hex, in both directions, and they've been Baseline across current Chrome, Firefox, and Safari since September 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new methods at a glance
&lt;/h2&gt;

&lt;p&gt;There are six of them, split into three pairs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bytes.toBase64(options)&lt;/code&gt; and &lt;code&gt;Uint8Array.fromBase64(string, options)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bytes.toHex()&lt;/code&gt; and &lt;code&gt;Uint8Array.fromHex(string)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;target.setFromBase64(string, options)&lt;/code&gt; and &lt;code&gt;target.setFromHex(string)&lt;/code&gt; for writing into an existing buffer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the old helper next to its replacement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: works, but only by pretending bytes are Latin-1 characters&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bytesToBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;base64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "after" version isn't just shorter. The spread in &lt;code&gt;String.fromCharCode(...bytes)&lt;/code&gt; passes every byte as a separate function argument, so a large file can blow past the engine's argument limit and throw a &lt;code&gt;RangeError&lt;/code&gt;. The native methods don't have that ceiling, and they skip the intermediate "binary string" entirely.&lt;/p&gt;

&lt;p&gt;Hex gets the same treatment, which makes Web Crypto output a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Uint8Array.fromHex()&lt;/code&gt; goes the other direction and throws a &lt;code&gt;SyntaxError&lt;/code&gt; on odd-length strings or non-hex characters, so you get validation for free instead of silently producing &lt;code&gt;NaN&lt;/code&gt; bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  base64url and padding without string surgery
&lt;/h2&gt;

&lt;p&gt;Standard base64 uses &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;, which break in URLs and filenames. The usual fix is a chain of &lt;code&gt;.replace()&lt;/code&gt; calls to swap in &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; and strip the trailing &lt;code&gt;=&lt;/code&gt;. Now it's an option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;omitPadding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. 'q3N8y0Zp1VbX-7k2_JmR4g'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding takes the same &lt;code&gt;alphabet&lt;/code&gt; option. That makes peeking inside a JWT payload pleasantly boring, since JWT segments are base64url with the padding removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readJwtPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works because the default &lt;code&gt;lastChunkHandling&lt;/code&gt; mode, &lt;code&gt;"loose"&lt;/code&gt;, accepts a final chunk of two or three characters without padding. If you're validating input you don't control and want to reject anything non-canonical, pass &lt;code&gt;lastChunkHandling: 'strict'&lt;/code&gt;: the last chunk must then be padded to four characters, and any leftover "overflow bits" must be zero. Decoding also ignores ASCII whitespace, so base64 that was wrapped across lines in a PEM file or an email decodes without a cleanup pass.&lt;/p&gt;

&lt;p&gt;One more quiet win: because you're decoding to bytes, not to a string, there's no more &lt;code&gt;InvalidCharacterError&lt;/code&gt; from feeding &lt;code&gt;btoa&lt;/code&gt; a string with an emoji in it. Encode text with &lt;code&gt;TextEncoder&lt;/code&gt; first, then call &lt;code&gt;toBase64()&lt;/code&gt;, and UTF-8 just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding into memory you already have
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;setFrom*&lt;/code&gt; methods write into an existing &lt;code&gt;Uint8Array&lt;/code&gt; and return &lt;code&gt;{ read, written }&lt;/code&gt; so you know how much input was consumed and how many bytes landed. That's handy when you're reusing a buffer or decoding base64 that arrives in pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;written&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastChunkHandling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stop-before-partial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;"stop-before-partial"&lt;/code&gt;, an incomplete trailing chunk is left alone instead of being decoded early or throwing, so the next network chunk can finish it. The &lt;code&gt;read&lt;/code&gt; count tells you exactly where to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for them (and when not yet)
&lt;/h2&gt;

&lt;p&gt;If your app targets current evergreen browsers, you can use these today and delete the helpers. On the server, check your runtime version before you rip out &lt;code&gt;Buffer.from(x, 'base64')&lt;/code&gt;. Node's &lt;code&gt;Buffer&lt;/code&gt; still works fine, but these methods give you one API that runs the same in the browser, Deno, Bun, and edge runtimes.&lt;/p&gt;

&lt;p&gt;For older targets, feature-detect and fall back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toBase64&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pull in a polyfill: &lt;code&gt;core-js&lt;/code&gt; covers the full set, and the &lt;code&gt;es-arraybuffer-base64&lt;/code&gt; shim does too.&lt;/p&gt;

&lt;p&gt;The takeaway is small but satisfying: bytes can finally stay bytes. The next time you reach for &lt;code&gt;btoa&lt;/code&gt;, check whether what you actually have is a &lt;code&gt;Uint8Array&lt;/code&gt;. If it is, &lt;code&gt;toBase64()&lt;/code&gt; is almost certainly the better call. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64" rel="noopener noreferrer"&gt;MDN reference for &lt;code&gt;Uint8Array.fromBase64()&lt;/code&gt;&lt;/a&gt; covers every option in detail, including the edge cases around padding and overflow bits.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webapis</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>import defer: Lazy Modules Without Going Async</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:12:03 +0000</pubDate>
      <link>https://dev.to/grimicorn/import-defer-lazy-modules-without-going-async-22mf</link>
      <guid>https://dev.to/grimicorn/import-defer-lazy-modules-without-going-async-22mf</guid>
      <description>&lt;p&gt;Every codebase has that one import. A syntax highlighter, a PDF renderer, a date-formatting library with a locale table the size of a small novel. It sits at the top of a file, it gets evaluated the moment the module graph runs, and it is needed on exactly one code path that most users never hit.&lt;/p&gt;

&lt;p&gt;The usual fix is &lt;code&gt;await import()&lt;/code&gt;. It works, but it charges rent: the function that needed the module becomes async, and so does its caller, and its caller's caller. You wanted to move some work later in time, and instead you rewrote a call stack. &lt;code&gt;import defer&lt;/code&gt; is the piece that was missing, and it is riding along with ES2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linked up front, evaluated on first touch
&lt;/h2&gt;

&lt;p&gt;The syntax is a single keyword in one position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;typescript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compileFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The typescript module graph evaluates here, on first property access.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createProgram&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing from &lt;code&gt;typescript&lt;/code&gt; runs at startup. The module and its dependencies are still resolved, fetched, parsed, and linked, so a missing file or a bad named import still blows up immediately rather than lurking. What is deferred is evaluation: the top-level code only runs the first time you read a property off &lt;code&gt;ts&lt;/code&gt;. If &lt;code&gt;compileFile&lt;/code&gt; is never called, that graph never executes.&lt;/p&gt;

&lt;p&gt;That distinction is the whole value proposition. &lt;code&gt;import()&lt;/code&gt; defers everything, which is why it has to be async. &lt;code&gt;import defer&lt;/code&gt; defers only the synchronous part that can safely be moved, so property access stays synchronous and nothing above it needs to change colour.&lt;/p&gt;

&lt;p&gt;Only the namespace form works. There is no &lt;code&gt;import defer { createProgram } from "typescript"&lt;/code&gt; and no default form, because the namespace object is the thing doing the work — evaluation is triggered by a property read, and a destructured binding has nothing left to intercept.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that will surprise you
&lt;/h2&gt;

&lt;p&gt;The deferred namespace is effectively a proxy, and the list of operations that trip evaluation is broader than "read a property". Reading a key triggers it, but so does &lt;code&gt;"value" in ns&lt;/code&gt;, &lt;code&gt;Object.keys(ns)&lt;/code&gt;, a &lt;code&gt;for...in&lt;/code&gt;, &lt;code&gt;Object.getOwnPropertyDescriptor&lt;/code&gt;, and even &lt;code&gt;delete ns.value&lt;/code&gt; or &lt;code&gt;Object.defineProperty&lt;/code&gt; on it — the operation fails, since the namespace is sealed, but evaluation happens anyway. &lt;code&gt;Object.isSealed()&lt;/code&gt; and &lt;code&gt;Object.isFrozen()&lt;/code&gt; enumerate keys, so those count too.&lt;/p&gt;

&lt;p&gt;Which also means this defeats the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;squares&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./squares.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSquare&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;squares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// evaluates immediately — you read a property&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three behaviours are worth committing to memory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-level &lt;code&gt;await&lt;/code&gt; opts out.&lt;/strong&gt; Reading a property is synchronous, so it cannot wait on an async module. A module containing top-level &lt;code&gt;await&lt;/code&gt; is evaluated eagerly, along with whatever its evaluation requires. The rest of the graph can still stay deferred, but do not assume &lt;code&gt;defer&lt;/code&gt; bought you anything if the target awaits at the top level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side effects move.&lt;/strong&gt; If a module installs a polyfill, registers a custom element, or patches a global, deferring it means that side effect now happens at an unpredictable moment, or never. Those modules should stay on a plain &lt;code&gt;import&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation errors are cached and synchronous.&lt;/strong&gt; If the deferred module throws while initializing, the throw comes out of whatever expression touched the namespace, catchable with a normal &lt;code&gt;try...catch&lt;/code&gt;. Every later access rethrows the same error rather than retrying.&lt;/p&gt;

&lt;p&gt;One more detail that saves a debugging session: the namespace deliberately does not expose an export named &lt;code&gt;then&lt;/code&gt;, even after evaluation. Without that carve-out, handing the namespace to &lt;code&gt;Promise.resolve()&lt;/code&gt; would look like a thenable and force evaluation as a side effect of promise resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you can actually use it
&lt;/h2&gt;

&lt;p&gt;Tooling got there first, as usual. TypeScript has understood the syntax since 5.9, and Babel, webpack, and Prettier all handle it. Bun and Deno ship it enabled by default. In browsers, V8 has had it behind a flag with an Intent to Ship filed in September 2026, WebKit has it implemented, and Gecko has signalled support — so treat it as landing rather than landed, and check the compatibility table before you rely on it in production.&lt;/p&gt;

&lt;p&gt;The honest use case today is server-side and CLI code, where Bun, Deno, and a bundler give you a straight path. Go find the heaviest module on your cold-start path that is only needed conditionally, swap the import, and measure. If it turns out the module has top-level &lt;code&gt;await&lt;/code&gt;, or installs something on import, you have learned something useful about your dependency either way.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>performance</category>
      <category>tooling</category>
      <category>node</category>
    </item>
    <item>
      <title>Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:06:01 +0000</pubDate>
      <link>https://dev.to/grimicorn/turbopacks-persistent-build-cache-only-helps-if-your-ci-keeps-it-4dme</link>
      <guid>https://dev.to/grimicorn/turbopacks-persistent-build-cache-only-helps-if-your-ci-keeps-it-4dme</guid>
      <description>&lt;p&gt;You upgrade to Next.js 16.3, read that Turbopack's persistent file system cache is now on by default for &lt;code&gt;next build&lt;/code&gt;, see a chart claiming up to 5.5x faster builds, push to CI, and watch your pipeline take exactly as long as it did yesterday. Nothing is broken. The feature is doing precisely what it says. The problem is that "persistent" means persisted to a directory, and your build container throws that directory away the moment it exits.&lt;/p&gt;

&lt;p&gt;This is the least glamorous kind of performance work: the speedup is real, it's free, and collecting it is an infrastructure chore rather than a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache is a directory, not a feature flag
&lt;/h2&gt;

&lt;p&gt;Turbopack has been persisting its incremental compilation cache to disk for &lt;code&gt;next dev&lt;/code&gt; since 16.1. In 16.3 the same mechanism graduated for &lt;code&gt;next build&lt;/code&gt;, and both are enabled by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turbopackFileSystemCacheForDev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
    &lt;span class="na"&gt;turbopackFileSystemCacheForBuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev writes to &lt;code&gt;.next/dev/cache/turbopack&lt;/code&gt;, builds write to &lt;code&gt;.next/cache/turbopack&lt;/code&gt;. On the second build Turbopack reads those entries off disk before compiling anything new, so you only pay for what changed. Vercel's published numbers span a wide range depending on how much of the route graph the change touches: &lt;code&gt;nextjs.org&lt;/code&gt; went from 21s cold to 9.2s warm, &lt;code&gt;vercel.com/home&lt;/code&gt; from 66s to 46s, and &lt;code&gt;vercel.com/geist&lt;/code&gt; from 30s to 5.5s.&lt;/p&gt;

&lt;p&gt;Every one of those numbers assumes &lt;code&gt;.next/cache&lt;/code&gt; survives between runs. Restoring it in GitHub Actions is a handful of lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;~/.npm&lt;/span&gt;
      &lt;span class="s"&gt;${{ github.workspace }}/.next/cache&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}&lt;/span&gt;
    &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;restore-keys&lt;/code&gt; fallback matters more than the exact key. If your lockfile is unchanged but source files moved, you still want the previous cache as a starting point instead of a cold compile.&lt;/p&gt;

&lt;p&gt;Docker is the case that quietly eats this. A containerized build starts from a clean layer, so unless you explicitly mount a cache or use &lt;code&gt;RUN --mount=type=cache&lt;/code&gt;, &lt;code&gt;.next/cache&lt;/code&gt; is empty on every single build and Turbopack spends time writing a cache that nothing will ever read. If that describes your setup and you don't plan to fix it, turn the thing off rather than paying for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;turbopackFileSystemCacheForBuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the actual decision in front of you: persist the directory, or opt out. Leaving it on with nothing to restore is the one configuration that costs you something and returns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dev-memory win needs no setup at all
&lt;/h2&gt;

&lt;p&gt;The other half of the release is the part you'll notice without touching any config. Because cached results are now safely on disk, Turbopack can evict them from memory instead of holding every visited route forever. After compiling 50 routes, Vercel measured its own dashboard app dropping from 21.5 GB to 2 GB, and &lt;code&gt;nextjs.org&lt;/code&gt; from 4,600 MB to 840 MB.&lt;/p&gt;

&lt;p&gt;That's roughly a 90% and 82% reduction, and it lands in a moment when your dev machine is more crowded than it used to be. A coding agent, a TypeScript server, a linter, and a test watcher are all competing for the same RAM as your bundler. Eviction requires the dev file system cache to be enabled, and both are on by default. The escape hatch exists if you're debugging cache behavior itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;turbopackMemoryEviction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default is 'auto'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your mileage genuinely varies here. The reduction depends on the size of your route graph, how much of it you touched, and how long the session ran. A three-route side project has nothing to evict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller things worth a look
&lt;/h2&gt;

&lt;p&gt;The React Compiler has been stable in Next.js since 16.0, but only as a Babel transform, which meant large apps waited on JS execution during builds. The React team shipped a native Rust port, and Turbopack now wires it up behind an experimental flag with early tests on large apps showing 20-50% compilation wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;reactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turbopackRustReactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbopack also picked up Vite's &lt;code&gt;import.meta.glob&lt;/code&gt;, which is a small quality-of-life upgrade for anyone hand-maintaining a list of content files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./posts/*.mdx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;eager: true&lt;/code&gt; to import each match immediately. It's Turbopack-only, so it won't work if you're still building with &lt;code&gt;--webpack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Of the four, the build cache is the one with a real chance of being invisible to you. Before you file the 16.3 upgrade as done, go look at whether &lt;code&gt;.next/cache&lt;/code&gt; actually survives a CI run. If it doesn't, you've been paying for a cache you never read.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://nextjs.org/blog/next-16-3-turbopack" rel="noopener noreferrer"&gt;Turbopack 16.3 release post&lt;/a&gt;, the &lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache" rel="noopener noreferrer"&gt;&lt;code&gt;turbopackFileSystemCache&lt;/code&gt; reference&lt;/a&gt;, and the &lt;a href="https://nextjs.org/docs/app/guides/ci-build-caching" rel="noopener noreferrer"&gt;CI build caching guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>tooling</category>
      <category>performance</category>
      <category>bundlers</category>
    </item>
  </channel>
</rss>
