Users see an old version or chunks fail to load after a deploy
“Right after a release, error tracking fills with ChunkLoadError or "Failed to fetch dynamically imported module", some users still see last week's UI, and a hard refresh fixes it for them.”
THE PRINCIPLE
Hashed assets are immutable and cached forever; the HTML that points at them is revalidated on every load. Old assets stay available for a while after each deploy, and the app has a planned path for when a chunk is missing anyway.
FIRST MOVES
- Check the failing URL: a 404 (or an HTML fallback served with a JS URL) for a hash from the previous build confirms version skew.
- Serve index.html (and any SPA fallback route) with Cache-Control: no-cache, and hashed assets with max-age=31536000, immutable; verify at the CDN, not just the origin.
- Keep the previous one or two builds' assets deployed (or use the host's skew protection) so open tabs can finish loading lazy chunks.
- Handle the failure: listen for vite:preloadError (or catch ChunkLoadError in webpack apps) and reload once, guarded against reload loops.
- If a service worker precaches the app, ship an update flow: detect the waiting worker, prompt the user, then skipWaiting and reload on controllerchange.
TOOLS: THEN → NOW
Asking users to hard refreshno-cache HTML, immutable hashed assets, and a one-shot reload on chunk errors
skipWaiting() unconditionally on installA prompted update (Workbox messageSkipWaiting) so the page and worker do not mix versions
PATTERN SNAPSHOT
// Vite: a lazy chunk from an older build is gone; reload once into the new build.
window.addEventListener("vite:preloadError", (event) => {
const key = "reloaded-for-" + location.pathname;
if (sessionStorage.getItem(key)) return; // let the error surface
sessionStorage.setItem(key, "1");
event.preventDefault();
location.reload();
});
// Headers (CDN or host config):
// /index.html Cache-Control: no-cache
// /assets/* Cache-Control: max-age=31536000, immutableCLOSE THE AI. EXPLAIN THIS.
A user left a tab open over a deploy and clicks into a lazy route. Walk through why it fails, and which two server-side settings plus one client-side handler prevent it.HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- Vite: Building for production (Load error handling) (opens in new tab)Checked
- MDN: Cache-Control (cache busting) (opens in new tab)Checked
- Chrome for Developers: Handling service worker updates with immediacy (Workbox) (opens in new tab)Checked
- webpack source: JsonpChunkLoadingRuntimeModule (ChunkLoadError) (opens in new tab)Checked
Guide reviewed