Repository navigation
Description
Describe the bug
When a module is imported (through a barrel with "sideEffects": false) but its export is never used, Rollup/Rolldown tree-shakes its JavaScript, so no chunk mentions the asset. The asset itself is still written to the output folder. A plugin that globs the output folder (for example a service-worker precache) then sees it.
With a 4.6 MB .wasm this means every user downloads the file for a precache entry although no code can run it.
The asset is emitted while the module is transformed (vite:asset-import-meta-url -> fileToBuiltUrl -> pluginContext.emitFile({ type: 'asset' }), in packages/vite/src/node/plugins/asset.ts), before tree-shaking decides that the module is not part of the output. Nothing removes an emitted asset that no chunk and no other asset refers to afterwards.
Observed on Vite 8.3.6 (Rolldown 1.2.16). I did not check Vite 7 with Rollup.
Reproduction
package.json
{ "name": "repro", "private": true, "type": "module" }bun add vite@^8.3.5 # or npm i vite
mkdir -p node_modules/fake-lib src
head -c 4000000 /dev/urandom > node_modules/fake-lib/pdfium.wasmnode_modules/fake-lib/package.json
{ "name": "fake-lib", "version": "1.0.0", "type": "module", "main": "index.js", "sideEffects": false }node_modules/fake-lib/pdf.js
export const wasmUrl = new URL('./pdfium.wasm', import.meta.url).href;
export const loadPdf = () => fetch(wasmUrl);node_modules/fake-lib/render.js
export const render = (text) => `rendered:${text}`;node_modules/fake-lib/index.js
export { render } from './render.js';
export { loadPdf } from './pdf.js';src/main.js (uses only render)
import { render } from 'fake-lib';
document.body.textContent = render('hello');src/index.html
<!doctype html><html><body><script type="module" src="./main.js"></script></body></html>vite.config.mjs
import { defineConfig } from 'vite';
export default defineConfig({ root: 'src', build: { outDir: '../dist', emptyOutDir: true } });npx vite build
ls dist/assetsActual
dist/assets/pdfium-DuhTPA_B.wasm 4,000.00 kB
dist/assets/index-CZ04JbqN.js 0.73 kB
index-*.js does not contain pdfium or .wasm.
Expected
No pdfium-*.wasm in dist, as when the module is never imported. (If the library has side effects, so that new URL(...) stays in a chunk, the asset is referenced and must stay: the same build without "sideEffects": false emits it and the chunk names it.)
What I checked
- A module that nobody imports emits nothing, so only "imported, then tree-shaken" leaks.
- Assets referenced from CSS (
url(./font.woff2)), from HTML, and by a usednew URL('./used.png', import.meta.url)all stay, as they should. - An unused
import url from './unused.png?url'(value dropped by tree-shaking) leaks the same way. - A service-worker plugin that globs the output (
vite-plugin-pwa2.0.0 withglobPatterns: ['**/*.{js,css,html,wasm}']and a raisedmaximumFileSizeToCacheInBytes) lists the wasm in the precache manifest. With its default patterns and size cap it would not.
A fix that worked in a plugin
This plugin, enforce: 'post', removes the leak; it keeps everything listed above:
export const pruneUnreferencedAssets = () => ({
name: 'prune-unreferenced-assets',
apply: 'build',
enforce: 'post',
generateBundle(_options, bundle) {
const texts = Object.values(bundle).flatMap((it) =>
it.type === 'chunk' ? [it.code] : typeof it.source === 'string' ? [it.source] : [],
);
for (const [name, it] of Object.entries(bundle)) {
const isFileAsset =
it.type === 'asset' && !it.fileName.endsWith('.html') && (it.originalFileNames?.length ?? 0) > 0;
if (isFileAsset && !texts.some((text) => text.includes(it.fileName))) {
delete bundle[name];
}
}
},
});It is a text search, so it is a sketch. A real fix would use the references Vite already tracks (chunk.viteMetadata.importedAssets, the reference ids of emitFile) instead of scanning code. Not tested: assets emitted by worker builds, and a binary asset referenced only from another binary asset.
Question
Is dropping an emitted asset that no remaining chunk or asset references something Vite wants in core, or should it stay a plugin?
System info
Vite 8.3.6, Rolldown 1.2.16, Bun 1.4.4 on Linux (the reproduction does not depend on Bun).
Related
- CSS referenced assets are included even when the file is tree shaken #9073 is the same leak for assets referenced from CSS in a tree-shaken file.
- Nested worker not emitted when its module is tree-shaken from the main bundle (regression in 8.3.0-beta.0) #23894 is the opposite case for workers: a nested worker was not emitted when its module was tree-shaken.
Activity