An app whose shell no longer matches the server repairs itself - #130
Merged
suskozaver merged 1 commit intoSep 17, 2026
Merged
Conversation
Reported after a deploy: a white screen, "Application error: a client-side exception", and ChunkLoadError: Loading chunk 135 failed. The hashes said what happened. The browser was running webpack-55676dfde347fa72.js where the new build has webpack-9100694e6ef91c55.js, so the runtime came out of the service worker's cache. It asked for 135-d5de4c531d4e06a7.js and the new build calls that chunk 135-b4d0965ad68280be.js, so the request 404ed. The neighbouring 255-3d881dfa8c72bc56.js loaded fine, because that chunk had not changed between the builds and kept its name. Half old and half new, which is the exact thing the per-build cache exists to prevent. It does prevent it, for files it holds. What it cannot prevent is a chunk it never held. The cache fills by visiting, chunks are lazy, and a deploy takes the old ones off the server, so from that moment any chunk the old shell had not already cached is gone for good. The way out is the update prompt, and the update prompt lives inside the app that cannot boot. So this is a way out that does not live there. An inline script in the head, ahead of the app's own code, listening for that failure and only that one: drop the caches, unregister the worker, reload once. Explicit updates stay, because they are a good decision. This speaks up only when the shell is already broken and there is nothing left to protect. Twice in ten minutes and it stops. If clearing the cache did not help then the fault is on the server, and a page that reloads forever is one broken page turned into a machine hammering it. Inline rather than a component, because by the time React could mount a component the chunk it needs may be the missing one. And serialised from the real functions with toString() rather than the same rules typed out again as a string, because two copies of a rule is how one of them gets fixed. Eighteen tests, five of which run the serialised source in a sandbox with the browser stubbed out, since source that has never been run has never been checked. Verified in the built output: the listener is in the HTML of every prerendered page.
|
Someone is attempting to deploy a commit to the Arun's projects Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported after a deploy: a white screen, "Application error: a client-side exception has occurred", and
ChunkLoadError: Loading chunk 135 failedin the console.What the hashes said
255-…3d881dfa8c72bc563d881dfa8c72bc56135-…d5de4c531d4e06a7b4d0965ad68280bewebpack-…55676dfde347fa729100694e6ef91c55The runtime was the old one, out of the service worker's cache. It asked for chunk 135 by its old name and the new build calls that file something else, so the request 404ed. Chunk 255 loaded perfectly, because its contents had not changed between the builds and it therefore kept its name. Half old and half new, which is the exact thing the per-build cache exists to prevent.
It does prevent it, for files it holds. What it cannot prevent is a chunk it never held: the cache fills by visiting, chunks are lazy, and a deploy takes the old ones off the server. From that moment, any chunk the old shell had not already cached is gone for good.
And the way out is the update prompt, which lives inside the app that cannot boot.
The fix
A way out that does not live there. An inline script in the head, ahead of the app's own code, listening for that failure and only that one: drop the caches, unregister the worker, reload once.
Explicit updates stay, because they are a good decision and this does not touch them. The script speaks up only when the shell is already broken and there is nothing left to protect.
Twice in ten minutes and it stops. If clearing the cache did not help then the fault is on the server, and a page that reloads forever is one broken page turned into a machine hammering it.
Two things about the shape
Inline rather than a component, because by the time React could mount a component, the chunk it needs may be the missing one.
Serialised from the real functions with
toString()rather than the same rules typed out again as a string. Two copies of a rule is how one of them gets fixed and the other does not, and there is a test holding the script to the functions.Checks
npx tsc --noEmit,npx next lint,npm testandTZ=America/New_York npm test: 65 files, 1684 tests, no warnings.Eighteen of them are new. Five run the serialised source in a
node:vmsandbox with the browser stubbed out and assert what it does to the caches, the registration and the location, because source that has never been run has never been checked. The built output was also read: the listener is in the HTML of every prerendered page.