Contents
In brief
After an SPA deploy, a user with an open tab can hit a blank screen: React.lazy() requests a chunk with an old hash while the CDN already serves a new bundle. A Dev.to post shares a small global handler that reloads the page with a cache-busting query param.
What happened
Classic flow: the browser keeps an old index.html in memory; navigating to a route triggers a dynamic import that 404s or fails on the network. The console shows variants like Failed to fetch dynamically imported module or Loading chunk N failed — wording differs across Chrome, Safari, and older Webpack builds.
Most users will not hard-reload — they leave.
Why it matters
Code splitting saves first paint but ties shell version to chunk version. Any deploy without an HTML cache strategy leaves sessions stranded between releases.
| Symptom | Cause | Without a fix |
|---|---|---|
| Blank screen after navigation | Chunk removed/renamed | Lost conversions |
| Only some users affected | Stale tabs | Hard to reproduce in support |
| Hard reload helps | Fresh index.html with new hashes |
Does not scale |
Fifteen lines of handler code are cheaper than losing sessions after every release.
In practice
- Attach
window.addEventListener('error', …)with regexes for chunk, dynamic import, CSS chunk, and Safari’s «Importing a module script failed». - On match —
location.replace()with?_r=<timestamp>to fetch fresh HTML without an extra history entry (back button still works). - Set a «already reloaded» flag to avoid loops on a real network outage.
- Mirror logic on
unhandledrejectionif the lazy import fails inside a promise. - Test: deploy a new build, keep an old tab, navigate to a lazy route — the page should recover on its own.
Long term, pair this with cache policy: short TTL or no-cache for index.html, long cache for hashed assets.
Takeaway
React.lazy plus frequent deploys without recovery is a predictable incident. A global listener does not replace correct HTML caching, but it protects live tabs during rollout.

