evanw / evanw/esbuild

Minify does not DCE unused bindings in webpack-concatenated ESM barrel (terser does)

Open
#4,536 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.