跪拜 Guibai
← All articles
Frontend · Frontend Engineering · Interview

Why Old Browser Tabs Request Deleted JavaScript Chunks After a Deployment

By Revolution61 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A deployment that deletes old assets immediately breaks every browser tab that was opened before the release. For any app where users keep tabs open for hours, this turns a routine deploy into a stream of support tickets, and a blind refresh-on-error strategy masks the real failure while discarding unsaved user work.

Summary

When a single-page app uses route-based lazy loading, a user who never refreshes their tab is still running an old JavaScript entry point. After a new deployment replaces the build directory, that stale entry script continues to request the old hashed chunk filenames it knows about, which no longer exist on the server, producing a 404 and a `Failed to fetch dynamically imported module` error. A refresh fixes it because the browser then fetches the new HTML and the new entry script with updated chunk references.

The failure spans three layers: the deployment pipeline that deletes old assets, the caching strategy that determines how long HTML and hashed resources live, and the client-side recovery logic. Simply forcing a refresh on error hides the problem without revealing whether the deployment process is cutting off active sessions. Retaining old hashed assets for a transition window lets stale tabs finish loading, but it does not solve API incompatibility between old frontend code and new backend interfaces.

A reliable investigation starts with the Network tab: confirm the requested URL, the status code, and the initiator script. If the initiator is an old entry script and the requested hash no longer exists on the server, the deployment pipeline has broken the contract with running clients. The fix is a combination of deployment-side retention, correct `Cache-Control` headers that treat HTML as volatile and hashed assets as immutable, and a measured client-side recovery flow that logs the failure before refreshing, with safeguards against infinite loops and data loss.

Takeaways
An old browser tab keeps running the original entry script across deployments; it does not auto-update its JavaScript.
A dynamic import triggered after deployment still requests the old hashed chunk filename, which the server no longer has if old assets were deleted.
The Network tab's Initiator column reveals whether the request came from an old entry script, distinguishing a version mismatch from a path misconfiguration.
HTML should use `Cache-Control: no-cache` to revalidate on every navigation, while hashed JS/CSS can use long-lived `immutable` caching.
Retaining old hashed assets for a transition period lets stale tabs complete their lazy loads, but does not fix API contract changes between old frontend code and new backends.
A client-side refresh on `vite:preloadError` needs guardrails: error logging before refresh, a retry limit to prevent loops, and a check for unsaved user input.
Reproducing the bug requires opening a tab on the old version, deploying, and then triggering the lazy route without refreshing the tab.
A 200 response containing HTML instead of JavaScript points to a server fallback rule, not a missing chunk problem.
Conclusions

The core contract violation is between the deployment process and the running client: the server deletes assets that the client was explicitly told it could request later. Treating deployments as atomic directory swaps rather than incremental additions breaks the lazy-loading model.

Refresh-on-error is a recovery mechanism, not a root-cause fix. It hides the deployment pipeline's behavior and can mask a misconfigured build path or a CDN propagation delay that a simple reload won't cure.

The split caching strategy for HTML versus hashed assets is widely recommended but often implemented backwards in practice, where HTML gets cached aggressively and hashed files get short TTLs, creating exactly the stale-entry-script problem described here.

API versioning and static asset retention are usually managed by different teams, but a deployment that keeps old chunks while changing API response shapes still breaks the user experience. The two layers must be coordinated.

Concepts & terms
Content-hashed filenames
Build tools like Vite append a hash of the file's contents to the filename (e.g., `OrderDetail-a81f3.js`). When the source changes, the hash changes, producing a new URL. This makes long-term caching safe because the same URL always returns identical content.
Dynamic import
An ES module `import()` call that returns a Promise and loads the module only when the code path executes, rather than at initial page load. Used for route-based code splitting to reduce initial bundle size.
Cache-Control: no-cache
A directive that tells the browser it must revalidate the cached response with the server before using it, typically via a conditional request. It does not prevent caching; it prevents using the cached copy without checking for a newer version.
Cache-Control: immutable
A directive indicating the response body will never change for the lifetime of the cache entry. It prevents the browser from sending even a conditional revalidation request during a page reload, reducing unnecessary network traffic for versioned assets.
vite:preloadError
A custom DOM event that Vite dispatches when a dynamically imported module fails to load. Applications can listen for it to implement custom error recovery, such as logging the failure and triggering a page reload.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗