Minify does not DCE unused bindings in webpack-concatenated ESM barrel (terser does)
- Dominant language
- Go
- Stars
- 40.1k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
When webpack 5 concatenates a large ESM barrel (e.g. @remixicon/react@4.8.0 [index.mjs](https://github.com/evanw/esbuild/issues/new#), ~2.4 MB, 3188 named exports, "sideEffects": false) and uses esbuild as the production minifier, unused icon components are not dead-code-eliminated. The same pipeline with terser as the minifier removes them.
This matches the documented webpack behavior that [optimization.usedExports](https://github.com/evanw/esbuild/issues/new#) relies on the minifier for statement-level DCE, and prior discussion in [#639](https://github.com/evanw/esbuild/issues/639) / [esbuild-loader#178](https://github.com/privatenumber/esbuild-loader/issues/178) that esbuild minify only removes unused top-level bindings in bundle mode — which does not apply when webpack wraps/concatenates modules.
Environment
@remixicon/react@4.8.0 (single [index.mjs](https://github.com/evanw/esbuild/issues/new#) barrel, static named exports)
Webpack 5.88.2 (via Umi 4.6 umi build)
esbuild used as minifier only (jsMinifier: 'esbuild', Umi 4 default)
Consumer imports one named export transitively: import { RiLoader4Line } from '@remixicon/react' (from @backstage/ui compiled ESM)
Observed behavior
Webpack usedExports is correct for the concatenated module: SET(1): RiLoader4Line.
Webpack still emits the entire concatenated [index.mjs](https://github.com/evanw/esbuild/issues/new#) into the chunk (CONCATENATED MODULE); DCE is deferred to the minifier.
esbuild minify leaves all 3188 icon factories in the production entry (e.g. unused RiZzzLine path data still present; remixicon className count = 3188). Entry chunk: ~3.9 MB / 923 KB gzip.
Switching only jsMinifier to 'terser' (webpack's traditional default): unused icons removed, remixicon count drops to ~10, entry 375 KB gzip.
Control: esbuild in isolation
A direct production bundle of import { RiLoader4Line } from '@remixicon/react' with esbuild (react external) is ~530 B — tree-shaking works when esbuild is the bundler, not merely the minifier after webpack concatenation.
Similarly, isolated Webpack 5.105 + terser for a single RiAddLine import is ~319 B (see [backstage/backstage#35398](https://github.com/backstage/backstage/pull/35398)).
Question / request
Is improved DCE for webpack-concatenated output in scope for esbuild's minifier, or is the recommended workaround still "use terser when webpack sets usedExports and concatenates large ESM barrels"?
This affects any host using webpack + esbuild-minify (notably Umi 4's default) that pulls in libraries with large single-file ESM barrels and named imports.
Related
https://github.com/evanw/esbuild/issues/639
https://github.com/privatenumber/esbuild-loader/issues/178
https://webpack.js.org/guides/tree-shaking/
https://github.com/backstage/backstage/issues/35397
Happy to provide a minimal reproduction repository if that would help.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the reported webpack-concatenated @remixicon/react index.mjs case and compare esbuild minification with terser, using the single RiLoader4Line import as the entry point. Read the prior discussion in esbuild#639 and esbuild-loader#178. Done means establishing whether unused bindings in this output are supported and documenting or testing the resulting behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, javascript, webpack
- Domain
- build-system, compilers, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100