ESM output bakes hashed chunk filenames into importing modules, defeating long-term caching (no opt-out)
Bug report
What is the current behavior?
Since 5.110, ESM output bakes a literal import("<chunk file>") next to the chunk id at every dynamic import site (RuntimeTemplate.analyzableChunkImport, __webpack_require__.ei):
// 5.104.1 — chunk id only; the runtime resolves the filename from its map
n.e(4458, "high")
// 5.110.3 — the hashed filename is baked into the importing module
s.ei(4458, () => import("/static/js/ton.2413d995a4.js"))Because the filename carries [contenthash], the importing module's own content now depends on the imported chunk's content, and that dependency propagates up the import graph into the initial wave. The chunk-id → filename map still exists in the runtime chunk, so the hashes are now stored twice.
This defeats long-term caching. optimization.runtimeChunk: 'single' exists precisely so that chunk filenames live in one small chunk and a change to a lazily loaded route invalidates only that chunk plus the runtime.
Measurement on a real application (~1850 chunks, ESM output, runtimeChunk: 'single', [contenthash] filenames). I changed one attribute in a single component that is only reachable through React.lazy, and rebuilt:
| 5.104.1 | 5.110.3 | |
|---|---|---|
| initial chunks invalidated | 4 of 9 | 5 of 9 |
| extra chunk invalidated | — | app.js, 1.07 MB raw / 197 KiB brotli |
| initial bytes a returning user refetches | 37 KiB gz (6%) | 288 KiB gz (46%) |
The diff of that 1.07 MB app.js between the two builds is exactly 10 bytes — the content hash of the lazily loaded chunk inside one baked import("/static/js/39137.<hash>.js"). Nothing else changed. Ten bytes of real change cost the user a 197 KiB brotli refetch.
There is a second, smaller cost: the baked URLs add ~195 KiB of raw JS to the initial wave (2 057 416 → 2 257 607 bytes). Compression hides most of it (+3.8 KiB brotli, −12.6 KiB gzip), so this one is minor, but it is still parsed on the main thread.
If the current behavior is a bug, please provide the steps to reproduce.
output.module: true,optimization.runtimeChunk: 'single',output.chunkFilenamecontaining[contenthash].- An entry chunk that holds modules doing
import()of lazily loaded routes. - Build, change one module that lives only in a lazily loaded chunk, rebuild.
- On 5.109 and earlier only the runtime chunk changes; on 5.110+ the entry chunk holding the importing modules changes too.
What is the expected behavior?
An opt-out. Baked specifiers are genuinely useful — a browser preload scanner or another bundler can follow them — so I am not asking to revert the behaviour, only to be able to choose. Something like output.analyzableChunkImport: false, or a value on the existing output.resourceHints group, would be enough.
There appears to be no way to turn it off today: supportsAnalyzable consults only output.module, build-time execution, and whether __webpack_public_path__ is reassigned; nothing in the schema, defaults.js or the docs mentions it. The output.chunkFilename documentation still describes the pre-5.110 behaviour ("these filenames need to be generated at runtime … need to add a mapping from chunk id to placeholder value to the output bundle with the webpack runtime").
As a workaround I patch analyzableChunkImport to return null, which is the method's own documented fallback to the runtime ensureChunk form. Measured, it restores the old profile exactly — 4 of 9 initial chunks and 6% refetched, app.js untouched — and the initial wave ends up smaller than on either version, because the 5.109/5.110 CommonJS tree-shaking gains are real and independent of the baking:
| initial wave | raw | gzip |
|---|---|---|
| 5.104.1 | 2 057 416 | 651 517 |
| 5.110.3 | 2 257 607 | 638 654 |
| 5.110.3 + workaround | 2 044 649 | 617 101 |
Other relevant information:
webpack version: 5.110.3 (5.104.1 for the comparison)
Node.js version: 25.9.0
Operating System: macOS 15 (arm64)
Additional tools: yarn 4.12.0, babel-loader, output.module: true, optimization.realContentHash on (production default)
Related: #22026 (also from this codebase).
Source: webpack/webpack