fix: force-load a module's new owning chunk when its only loaded chunk leaves a runtime during HMR#21131
Conversation
…k migrates runtimes Encodes the scenario described by the TODO in HotModuleReplacementPlugin (force load one of the chunks which contains the module): when a module's only loaded chunk is removed from a runtime but the module survives in that runtime via a not-yet-loaded async chunk, the update removes the chunk without disposing the module, leaving it orphaned.
When a module's only loaded chunk is removed from a runtime but the module still lives there via another (not-yet-loaded) chunk, the hot update removed the chunk without disposing the module, leaving it orphaned with no installed chunk owning it — so later HMR updates never reached that runtime. The plugin now emits the new owning chunk id in a manifest `f` field and the JS HMR runtime force-loads those chunks during the update. Implements the long-standing TODO in HotModuleReplacementPlugin.
…check Fold the force-load into the existing ensureChunkHandlers guard and rely on the idempotent ensure handlers (which skip already-installed chunks) instead of re-checking installedChunks.
…runtime check Adds tests for the cross-runtime dispose path and the non-unique hotUpdateMainFilename merge path (which now also carries force-load chunks), and collapses the runtime-membership check to a single expression.
Runs the force-load runtime branch end-to-end on the web target: a module migrates from a shared (multi-runtime) chunk into an async chunk on update, and the runtime force-loads it so the module stays installed. Gated to the web target since the node/webworker harness can't resolve the async chunk file mid-update.
…e collision Covers the branch that merges force-load chunk ids across runtimes when output.hotUpdateMainFilename is not unique per runtime: one runtime migrates a module into an async chunk (producing a force-load chunk) while another changes differently, so their updates collide and are merged (with the expected warning). Web target only, like the sibling force-load test.
🦋 Changeset detectedLatest commit: 121d021 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (996ce20). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@996ce20
yarn add -D webpack@https://pkg.pr.new/webpack@996ce20
pnpm add -D webpack@https://pkg.pr.new/webpack@996ce20 |
| if (forceLoadChunks) { | ||
| forceLoadChunks.forEach(function (chunkId) { | ||
| Object.keys($ensureChunkHandlers$).forEach(function (key) { | ||
| $ensureChunkHandlers$[key](chunkId, promises); |
There was a problem hiding this comment.
This is a false positive from analyzing the runtime template in isolation. $ensureChunkHandlers$ is a placeholder declared = undefined at the top of this file only so the template parses; at code generation it is string-substituted with __webpack_require__.f (an always-initialized object), so it is never undefined at runtime. The same placeholder is used right above this line ($ensureChunkHandlers$.$key$Hmr = …) and throughout the file, and the whole block is already gated by if ($ensureChunkHandlers$).
The iterate-and-call pattern here mirrors what __webpack_require__.e does internally — Object.keys(__webpack_require__.f).forEach(key => __webpack_require__.f[key](chunkId, promises)) — which also calls the handler without a typeof === "function" guard. Adding one here would be inconsistent with the rest of the runtime and add a dead check on the HMR update path, so I'm leaving it as-is.
Generated by Claude Code
Merging this PR will improve performance by 45.22%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Memory | benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
330 KB | 160.6 KB | ×2.1 |
| ⚡ | Memory | benchmark "context-esm", scenario '{"name":"mode-development","mode":"development"}' |
1,132.6 KB | 794.9 KB | +42.49% |
| ⚡ | Memory | benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
1,187.4 KB | 872.6 KB | +36.08% |
| ⚡ | Memory | benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
323.4 KB | 244.7 KB | +32.15% |
| ⚡ | Memory | benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' |
9 MB | 7.3 MB | +22.71% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/hmr-force-load-migrated-chunk (121d021) with main (cd45931)
5352ca0 to
121d021
Compare
Summary
Resolves the long-standing TODO in
lib/HotModuleReplacementPlugin.js("force load one of the chunks which contains the module"). In a multi-runtime build, when a module's only loaded chunk is removed from a runtime but the module survives there via a not-yet-loaded async chunk, the update removed the chunk (r) without disposing the module (mstayed empty) — leaving it loaded on the client with no installed chunk owning it, so later HMR updates to it were never delivered. The update manifest now carries a newf(force-load) field listing such chunks, and the runtime ensures them so the client keeps an installed owner. If the module no longer lives in the runtime at all, it is still disposed as before.Supersedes #21127 (same commits; branch renamed to the
fix/prefix to match the change kind).What kind of change does this PR introduce?
fix
Did you add tests for your changes?
Yes — unit tests in
test/HotModuleReplacementPlugin.test.js(force-load, dispose-when-gone, and cross-runtime merge on filename collision) plushotCasesintegration tests undertest/hotCases/chunks/force-load-migrated-chunkand.../force-load-runtime-collision.Does this PR introduce a breaking change?
No.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a
Use of AI
AI (Claude Code) was used to investigate the TODO, build the reproduction, implement the fix, and author the tests. All output was reviewed and verified by running the suite locally.
Generated by Claude Code