#22056·webpack

ESM output bakes hashed chunk filenames into importing modules, defeating long-term caching (no opt-out)

Author: Anonym-tskCreated Sep 11, 2026Updated Sep 14, 2026
Labelsarea: configarea: runtimeneeds: reproductionregression

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):

javascript
// 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.

  1. output.module: true, optimization.runtimeChunk: 'single', output.chunkFilename containing [contenthash].
  2. An entry chunk that holds modules doing import() of lazily loaded routes.
  3. Build, change one module that lives only in a lazily loaded chunk, rebuild.
  4. 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).