Repository navigation
RealContentHashPlugin watch rebuild can keep stale runtime chunk filename hashes #20710
Description
Activity
Looks like a bug, anyway using
RealContentHashPluginis strongly not recommended due performance reasonsI don't think
RealContentHashPluginis the actual issue here, it is using[contenthash]in the filename that causes the problem. I thinkRealContentHashPluginjust happens to detect something is wrong at its run stage, but its not to blame for an internal compilation Error.I'm trying to find another minimal reproduction that doesn't require
realContentHash: true(trueis the default inmode: "production"when[contenthash]is used in the filename.)I don't think RealContentHashPlugin is the actual issue here, it is using [contenthash] in the filename that causes the problem. I think RealContentHashPlugin just happens to detect something is wrong at its run stage, but its not to blame for an internal compilation Error.
Yeah, I think you are right here, just for information about performance
Reacted by David MurdochOk, so when using watch mode with a fixed-name service worker entry that loads split chunks via
importScripts, the second compilation will actually "succeed" but leave a stale chunk manifest embedded inservice-worker.js.I think it only happens with hashed chunk filenames. If I switch from
[contenthash]to[id]or[name], the issue goes away.To make the stale output visible, this repro sets
optimization.realContentHash = false. WithrealContentHash: true, webpack fails the second build instead of silently emitting the wrong manifest (repro for this is in the Issue description above)Minimal reproduction
src/service-worker.jsimport './shared.js'; console.log('service worker');
src/offscreen.jsimport './shared.js'; console.log('offscreen');
src/shared.jsconst bigTextBlob = " PUT A REALLY BIG STRING HERE TO FORCE CHUNKING, like all of lodash"; export const sharedValue = '__SHARED__'; console.log(sharedValue, bigTextBlob.length);
webpack.config.jsmodule.exports = { mode: 'production', target: 'webworker', cache: { type: 'memory' }, entry: { 'service-worker': { import: './src/service-worker.js', chunkLoading: 'import-scripts', filename: 'service-worker.js', }, offscreen: './src/offscreen.js', }, output: { filename: '[name].[contenthash].js', }, optimization: { // Disable RealContentHashPlugin so the rebuild completes and leaves // the stale manifest visible in service-worker.js. realContentHash: false, splitChunks: { chunks: 'all', name: 'js', minSize: 1, }, }, };
Rebuild sequence
- Start
compiler.watch(...) - Wait for the first successful build
- Rewrite
src/shared.js, changing__SHARED__to__SHARED__CHANGED, or whatever - Wait for the second build
- Compare:
- the current
service-workerentrypoint chunk file from stats - the
js.<hash>.jsfilename referenced inside emittedservice-worker.js
- the current
Observed result
The second build succeeds, and webpack emits a new shared chunk filename, but
service-worker.jsstill points at the previous chunk filename.Example shape:
Expected chunk file: js.<newhash>.js Referenced in service-worker.js: js.<oldhash>.js service-worker.js still points at the previous chunk filename
Expected result
On the second watch build, the manifest embedded in
service-worker.jsshould reference the newly emitted shared chunk filename.Notes
The PR, #20711 looks to solve both issues. I'm working on update the tests now.
- Start
@davidmurdoch thanks for detailed investigation, you can use this configuration for
watchTestsas a prof that it was fixed@davidmurdoch you are working on this currently??
- added a commit that references this issue
on Mar 28, 2026 Hi @davidmurdoch, thanks for the feedback. It looks like this PR #20724 can fix both #20710 and issue #19439.
- added a commit that references this issue
on Mar 28, 2026 I looked into this and traced the root cause to the chunk hashing order in
Compilation.createHash().Runtime chunks were being hashed before initial/split chunks, even though
GetChunkFilenameRuntimeModule(which generates__webpack_require__.u— the chunk ID → filename mapping) readschunk.contentHashfor each split chunk at hash time. Because those split chunks hadn't been hashed yet, the module embedded a stale hash from the previous compilation. On the next watch rebuild, the asset cache returned the previous runtime asset unchanged, so the runtime pointed to a filename that no longer existed on disk.This contradicts a comment already in the same method: "all non-runtime chunks need to be hashed first, since runtime chunk might use their hashes."
Fix in PR #20730 — chunks are now hashed in four explicit passes that respect the dependency chain:
- Async chunks
- Non-entry initial chunks (split chunks)
- Runtime chunks
- Entry chunks (may depend on runtime chunk hash)
Reacted by David MurdochReacted by David MurdochAwesome to see! Looks like my original solution was close, but not quite there. Nice!
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Bug report
When using watch mode with
cache: { type: "memory" },optimization.runtimeChunk: "single", andsplitChunksforcing a named shared chunk, the second compilation can fail inRealContentHashPluginafter a source edit.RealContentHashPluginis only where the stale reference gets detected. The stale hash appears to be baked into the runtime chunk before the changed initial chunk has been re-hashed.Minimal reproduction
async-entry.jssync-entry.jslarge-shared.jswebpack.config.jsRebuild sequence:
compiler.watch(...)large-shared.js, changing__SHARED__to__SHARED__CHANGEDObserved result:
Expected result:
The second watch build should succeed.
Suspected cause
In
lib/Compilation.js,createHash()includes this comment:But the implementation currently hashes chunks in this order:
That seems backwards for runtime modules like
GetChunkFilenameRuntimeModule, which are markeddependentHashand can embed initial chunk hashes. On rebuild, the runtime chunk can therefore keep a stale intermediate hash for a changed initial chunk, andRealContentHashPluginlater reports that stale reference.