<?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: Yash Bavadiya</title>
    <description>The latest articles on DEV Community by Yash Bavadiya (@xevrion).</description>
    <link>https://dev.to/xevrion</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%2F3625258%2F878f3f73-6763-4ec8-9d3a-9ed0de0959fb.jpg</url>
      <title>DEV Community: Yash Bavadiya</title>
      <link>https://dev.to/xevrion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xevrion"/>
    <language>en</language>
    <item>
      <title>One dot, one dash, and 131 kB shipped twice</title>
      <dc:creator>Yash Bavadiya</dc:creator>
      <pubDate>Tue, 28 Jul 2026 11:39:24 +0000</pubDate>
      <link>https://dev.to/xevrion/one-dot-one-dash-and-131-kb-shipped-twice-541g</link>
      <guid>https://dev.to/xevrion/one-dot-one-dash-and-131-kb-shipped-twice-541g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nuxt/nuxt" rel="noopener noreferrer"&gt;Nuxt&lt;/a&gt; is the Vue meta-framework. It sits on top of Vite, and a lot of what it does at build time is configure Vite carefully enough that you never have to think about it.&lt;/p&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/nuxt/nuxt/issues/22966" rel="noopener noreferrer"&gt;#22966&lt;/a&gt; was filed in August 2023. Importing the same WebAssembly binary from app code and from a web worker made the build emit two copies of it, identical bytes under two filenames, both shipped to the browser. It stayed open for three years.&lt;/p&gt;

&lt;p&gt;It is fixed now. PR &lt;a href="https://github.com/nuxt/nuxt/pull/35866" rel="noopener noreferrer"&gt;#35866&lt;/a&gt; was merged into &lt;code&gt;nuxt/nuxt&lt;/code&gt; as &lt;a href="https://github.com/nuxt/nuxt/commit/df18c4a8f1fa9b8577d3cc29a8965f6449adf698" rel="noopener noreferrer"&gt;&lt;code&gt;df18c4a&lt;/code&gt;&lt;/a&gt;, and #22966 closed with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;Nuxt overrides Vite's asset naming for the client build, so assets come out as &lt;code&gt;_nuxt/[name].[hash].[ext]&lt;/code&gt; rather than Vite's default &lt;code&gt;[name]-[hash][extname]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Vite builds workers in a separate sub-build with its own output configuration. Nuxt set the naming pattern on the client build and never passed it down to that sub-build, so the worker build kept Vite's default.&lt;/p&gt;

&lt;p&gt;That only matters when one asset is reachable from both graphs. In my repro the client build emitted &lt;code&gt;shared.Day3YyQn.wasm&lt;/code&gt; and the worker build emitted &lt;code&gt;shared-Day3YyQn.wasm&lt;/code&gt;. Same content hash, same 131,089 bytes, same sha256 (&lt;code&gt;313d428c...&lt;/code&gt;), one dot and one hyphen apart. Rollup deduplicates within a build rather than across two of them, so nothing catches it and the dist ships both.&lt;/p&gt;

&lt;p&gt;The runtime cost is worse than the disk cost. Two distinct URLs means the browser cannot reuse the first download for the second request, so a cold load transfers the binary twice even though &lt;code&gt;_nuxt&lt;/code&gt; assets are served &lt;code&gt;immutable&lt;/code&gt;. The app fetches one copy, the worker fetches the other, and the cache never gets a chance to help.&lt;/p&gt;

&lt;p&gt;Nothing errors and nothing warns. The build is green, the app works, and the only symptom is one extra file in a directory listing you have no particular reason to read. That is most of why it lasted three years.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7crwv30br00rypr2z9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7crwv30br00rypr2z9i.png" alt="before" width="799" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Merged PR: &lt;a href="https://github.com/nuxt/nuxt/pull/35866" rel="noopener noreferrer"&gt;https://github.com/nuxt/nuxt/pull/35866&lt;/a&gt;&lt;br&gt;
Merge commit: &lt;a href="https://github.com/nuxt/nuxt/commit/df18c4a8f1fa9b8577d3cc29a8965f6449adf698" rel="noopener noreferrer"&gt;&lt;code&gt;df18c4a8f1fa9b8577d3cc29a8965f6449adf698&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
Closes: &lt;a href="https://github.com/nuxt/nuxt/issues/22966" rel="noopener noreferrer"&gt;https://github.com/nuxt/nuxt/issues/22966&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole fix is in &lt;code&gt;packages/vite/src/vite.ts&lt;/code&gt;. The naming function was inline in the client build's output options, so the first move was to lift it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+import type { PreRenderedAsset } from 'rolldown'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L464-L478
+  const assetFileNames = nuxt.options.dev
+    ? undefined
+    : (chunk: PreRenderedAsset) =&amp;gt; withoutLeadingSlash(join(nuxt.options.app.buildAssetsDir, `${sanitizeFilePath(filename(chunk.names[0]!))}.[hash].[ext]`))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point both builds at the same value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;       build: {
         rolldownOptions: {
           output: {
             sanitizeFileName: sanitizeFilePath,
&lt;span class="gd"&gt;-            // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L464-L478
-            assetFileNames: nuxt.options.dev
-              ? undefined
-              : chunk =&amp;gt; withoutLeadingSlash(join(nuxt.options.app.buildAssetsDir, `${sanitizeFilePath(filename(chunk.names[0]!))}.[hash].[ext]`)),
&lt;/span&gt;&lt;span class="gi"&gt;+            assetFileNames,
&lt;/span&gt;           },
         },
       },
&lt;span class="gi"&gt;+      worker: {
+        rolldownOptions: {
+          output: {
+            // matching the main build ensures assets shared with workers are emitted only once
+            assetFileNames,
+            sanitizeFileName: sanitizeFilePath,
+          },
+        },
+      },
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both builds now resolve the shared asset to the same name, so it is emitted once. User-supplied &lt;code&gt;vite.worker&lt;/code&gt; options still take precedence through the &lt;code&gt;mergeConfig&lt;/code&gt; that was already there, so this sets a default rather than taking the option away from anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Finding it
&lt;/h3&gt;

&lt;p&gt;I did not trip over this in a project. I went looking for it.&lt;/p&gt;

&lt;p&gt;The contest wanted a fix with a defensible before/after number, so I started at Nuxt's issue tracker rather than at code, filtering for open, unassigned, triaged by a maintainer, and measurable in something honest like bytes. The first thing that filter told me was that &lt;code&gt;nuxt/nuxt&lt;/code&gt; has no open unassigned issues labelled bug at all. The team clears confirmed bugs fast. What is left is the long tail of p3-minor things nobody has gotten around to, which is exactly where you should be shopping.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#22966&lt;/code&gt; had been open since August 2023. Import the same WebAssembly file from your app and from a web worker, and the production build ships two full copies of it. Someone had confirmed it still reproduced eleven days before I found it. There was a competing PR too, closed by its author four days earlier, which I read anyway, because a closed PR is a free map of the terrain.&lt;/p&gt;

&lt;p&gt;My first repro did not reproduce anything. A minimal valid wasm file is eight bytes, eight bytes is well under Vite's 4 kB inline limit, and my duplicated asset quietly became a base64 string inside a JS chunk. Take two used a 131 kB binary padded with garbage. Then the worker build failed outright, because I had written top-level await and Vite bundles workers as IIFE by default, where that is not legal.&lt;/p&gt;

&lt;p&gt;Both wrong turns were the same lesson in different clothes. The worker build and the app build are separate pipelines with separate defaults. I did not know yet that this was the answer, but I had now been told twice.&lt;/p&gt;

&lt;p&gt;Then a build finished and the answer was sitting in the directory listing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shared.Day3YyQn.wasm    131 kB
shared-Day3YyQn.wasm    131 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same name, same size, same content hash &lt;code&gt;Day3YyQn&lt;/code&gt; in both filenames, identical sha256. One character of difference, a dot against a dash. That killed my first theory on the spot: if the hashes matched, this was not two assets and not a content-tracking bug. Both builds agreed exactly on what the file was. They disagreed on what to call it.&lt;/p&gt;

&lt;p&gt;So I grepped the builder for where names come from. &lt;code&gt;assetFileNames&lt;/code&gt; appears once in the entire package, in &lt;code&gt;packages/vite/src/vite.ts&lt;/code&gt;, setting Nuxt's pattern for the client build. &lt;code&gt;worker&lt;/code&gt; appears zero times. The absence was the bug. Nuxt renames assets for the main build, nothing tells the worker sub-build, it falls back to Vite's default, and the bundler cannot collapse two files whose names do not match.&lt;/p&gt;

&lt;p&gt;The fix was almost anticlimactic after that. Hand the worker build the same naming function the app build already uses. Three years open, one character of symptom, one config block.&lt;/p&gt;

&lt;h3&gt;
  
  
  One value, not two copies
&lt;/h3&gt;

&lt;p&gt;The obvious fix is to paste the naming function into the worker config as well. That works and I did not do it, because two copies of a naming rule is exactly how the two builds drift apart again. Someone tweaks the client pattern in a year, does not know a worker copy exists, and the bug comes back wearing different clothes. Hoisting it to a single &lt;code&gt;const&lt;/code&gt; and referencing it from both places means there is one rule and nowhere for a second one to hide.&lt;/p&gt;

&lt;p&gt;Lifting it out had one consequence worth mentioning. Inline, the &lt;code&gt;chunk&lt;/code&gt; parameter was contextually typed by the option it was being assigned to. Hoisted, that context is gone and TypeScript has nothing to infer from, so the annotation has to be explicit and &lt;code&gt;PreRenderedAsset&lt;/code&gt; gets imported from &lt;code&gt;rolldown&lt;/code&gt; (Nuxt's Vite runs on rolldown, hence &lt;code&gt;rolldownOptions&lt;/code&gt; rather than &lt;code&gt;rollupOptions&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The dev guard comes along for free. &lt;code&gt;assetFileNames&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; in dev, and since the worker build now reads the same value, dev behaviour is unchanged for both builds rather than needing its own branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The test is the real deliverable
&lt;/h3&gt;

&lt;p&gt;The change is small enough to be unconvincing on its own. Looking at a few lines of Vite options, a reviewer has no way to tell whether it fixes the reported problem or just shuffles names around.&lt;/p&gt;

&lt;p&gt;So the PR ships a fixture at &lt;code&gt;test/fixtures/worker-assets&lt;/code&gt;. An app component imports the asset directly, a module worker imports the same file and posts its URL back:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;wasmUrl&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;./shared.wasm?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workerUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&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="nf"&gt;onMounted&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;worker&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;Worker&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.ts&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&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="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;The fixture config sets &lt;code&gt;assetsInlineLimit: 0&lt;/code&gt;. Without it the test asset is small enough that Vite inlines it as a data URI, no file is emitted at all, and the assertion passes for a reason that has nothing to do with the fix. A test that is green on &lt;code&gt;main&lt;/code&gt; for the wrong reason is worse than no test.&lt;/p&gt;

&lt;p&gt;The test 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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emits a single copy of an asset imported from both app and worker&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;assetsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&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;_nuxt&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;files&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;readdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetsDir&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;wasmFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasmFiles&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.js&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&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="nx"&gt;assetsDir&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;referencedAssets&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;code&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;code&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;.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;wasm/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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;referencedAssets&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasmFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// the worker chunk and an app chunk must each point at the single emitted copy&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;referencingChunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;code&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;code&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="nx"&gt;wasmFiles&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="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referencingChunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referencingChunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting files alone would pass if the fix emitted one copy and left the worker pointing at a name that no longer exists, which is a worse bug than the one being fixed. The reference assertions exist to catch that.&lt;/p&gt;

&lt;p&gt;It fails on &lt;code&gt;main&lt;/code&gt; and passes with the change. That is what makes the diff trustworthy, and it is what stops this coming back the next time someone touches worker config.&lt;/p&gt;

&lt;h3&gt;
  
  
  A prior attempt
&lt;/h3&gt;

&lt;p&gt;PR &lt;a href="https://github.com/nuxt/nuxt/pull/35828" rel="noopener noreferrer"&gt;#35828&lt;/a&gt; went after the same bug earlier and was closed by its author. I found it while triaging, read the diff, and implemented independently. The difference in mine is the regression test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Responding to review
&lt;/h3&gt;

&lt;p&gt;CodeRabbit reviewed the PR and flagged a gap in the test's coverage. I checked the finding against the code instead of applying the suggested patch, because automated reviewers produce real bugs and confident nonsense in about equal measure and reading the lines yourself is the only way to tell which one you got. This one was right, so I pushed a follow-up commit (&lt;code&gt;test: check app and worker chunks reference the shared asset&lt;/code&gt;) tightening the assertion. Follow-up rather than a force-push, so the reviewer could see what had changed since they looked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbers
&lt;/h3&gt;

&lt;p&gt;Measured on a separate benchmark playground on my &lt;code&gt;contest/dedupe-worker-assets&lt;/code&gt; branch, not the test fixture. The app and a module web worker both import a 131,089 byte &lt;code&gt;shared.wasm&lt;/code&gt; via &lt;code&gt;?url&lt;/code&gt; and instantiate it with &lt;code&gt;WebAssembly.instantiateStreaming&lt;/code&gt;. Three builds per side, rebuilding &lt;code&gt;@nuxt/vite-builder&lt;/code&gt; for each side. Output was byte-identical across all runs, so the median is every run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;th&gt;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.wasm&lt;/code&gt; files in &lt;code&gt;.output/public/_nuxt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-1 file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bytes of those files&lt;/td&gt;
&lt;td&gt;262,178&lt;/td&gt;
&lt;td&gt;131,089&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-131,089 (-50%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total &lt;code&gt;.output/public&lt;/code&gt; bytes&lt;/td&gt;
&lt;td&gt;398,045&lt;/td&gt;
&lt;td&gt;266,956&lt;/td&gt;
&lt;td&gt;-131,089 (-32.9% of this fixture's client dist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cold-load wasm bytes transferred&lt;/td&gt;
&lt;td&gt;262,178&lt;/td&gt;
&lt;td&gt;131,089&lt;/td&gt;
&lt;td&gt;-131,089&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both copies in the before state carried the same sha256, &lt;code&gt;313d428c...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also checked it end to end rather than trusting the file listing. Before the fix, the app instantiates &lt;code&gt;/_nuxt/shared.Day3YyQn.wasm&lt;/code&gt; while the worker instantiates &lt;code&gt;/_nuxt/shared-Day3YyQn.wasm&lt;/code&gt;, and both URLs return 200 with content-length 131089. After, both instantiate the same URL successfully and the old duplicate name returns 404.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5pkth2acty0ycmrgqr0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5pkth2acty0ycmrgqr0o.png" alt="after" width="799" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What these numbers do not cover:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Savings apply only to apps that import the same asset from both the app and a web worker (any asset type, not just wasm). Apps without shared worker assets see zero change.&lt;/li&gt;
&lt;li&gt;The absolute byte number scales with the size of the shared asset. 131 KB is the synthetic fixture's asset, not a universal figure. Real-world wasm modules are commonly larger.&lt;/li&gt;
&lt;li&gt;No build-time or runtime CPU claims are made. The fix changes file naming only.&lt;/li&gt;
&lt;li&gt;Measured with the Vite (rolldown) builder only. The webpack and rspack builders are untouched by the fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it regenerates from &lt;a href="https://github.com/xevrion/nuxt/blob/contest/dedupe-worker-assets/bench/run.sh" rel="noopener noreferrer"&gt;&lt;code&gt;bench/run.sh&lt;/code&gt;&lt;/a&gt;, including raw logs, per-run sha256 lists, and HTTP transcripts, so any number above can be checked rather than taken on trust.&lt;/p&gt;

&lt;p&gt;CI's own benchmark suite (CodSpeed) reported no performance change. Its footnotes are worth reading though: no successful run existed on the true base commit, so it compared against a nearby one, and three skipped benchmarks used substituted baselines. I would call that corroboration that nothing regressed rather than proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope
&lt;/h3&gt;

&lt;p&gt;One value hoisted, one config block added, one fixture, one test. No refactoring of the code around it, no unrelated cleanups, nothing bundled in that a reviewer had not asked for. A three-year-old bug is tempting to treat as license for a bigger change, and I think the reason this merged in hours rather than sitting in a queue is that there was almost nothing in the diff to argue about.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
  </channel>
</rss>
