Why Old Browser Tabs Request Deleted JavaScript Chunks After a Deployment
This is the 10th article in the "Frontend Engineering On-Site" series. The following uses an easy-to-understand Vue 3 and Vite single-page project as a hypothetical scenario. Build filenames, request logs, cache response headers, and deployment processes are simplified examples and do not correspond to any specific online project.
Imagine a backend system using route-based lazy loading. A user opens the homepage in the morning and never refreshes. In the afternoon, a new version of the project is deployed. The user then clicks on Order Details, but the page fails to open, and the console shows the following error:
TypeError: Failed to fetch dynamically imported module:
https://static.example.com/assets/OrderDetail-a81f3.js
In the browser's Network tab, you can also see:
GET /assets/OrderDetail-a81f3.js 404
Strangely, after the user refreshes the page and navigates to the order details again, the functionality returns to normal.
If you only look at the result after a refresh, it's easy to blame the browser cache or simply execute a refresh inside the route error callback. This might allow some users to continue operating, but it doesn't explain where the old filename came from, nor does it clarify whether the same issue will occur on the next deployment.
This investigation requires placing the page's runtime and the deployment process on the same timeline:
User opens the old version
→ Browser runs the old entry script
→ New version of the project is deployed
→ Old build artifacts are deleted
→ User triggers a route that hasn't been loaded yet
→ Old script continues to request the old file
→ Server returns 404
This error spans the user's page, build artifacts, and deployment directory. Looking at only the browser or the server side is incomplete. Below, the changes to the old page and the new version are placed on the same timeline.
This article mainly analyzes why an old page retains old resource addresses, how the deployment process invalidates old resources, how caching and build artifacts should cooperate, and what problems remain even after a client-side refresh recovery.
1. Why Checking Version Differences After a Refresh Recovery Is Worthwhile
The order details route uses a dynamic import:
const routes = [
{
path: '/orders/:id',
component: () =>
import('@/views/orders/OrderDetail.vue')
}
]
This code is in the route configuration phase. The order details module does not load at the same time as the homepage code; the browser typically requests the corresponding build artifact when the user navigates to that route.
The following filename does not appear in the source code:
OrderDetail-a81f3.js
It is generated during the production build phase. The hash in the filename changes with the corresponding content and dependencies, so two builds can produce different results.
Assume the old version's build artifacts are:
assets/index-3c71e.js
assets/OrderDetail-a81f3.js
And the new version's build artifacts are:
assets/index-8f20d.js
assets/OrderDetail-f92c7.js
The old tab has already loaded and executed index-3c71e.js. This entry script contains the old version's module loading relationships. When navigating to the order details later, it will still request:
/assets/OrderDetail-a81f3.js
After refreshing the page, the browser re-fetches the current HTML and the new entry script. The address recorded in the new entry script has already changed to:
/assets/OrderDetail-f92c7.js
Therefore, recovery after a refresh is an important clue for judging version differences.
It alone cannot prove the problem is definitely from an old version. Dynamic module loading failures can also be related to network interruptions, incorrect resource paths, abnormal server responses, or browser extension blocking. The next step is still to check the failed request's URL, status code, and response content.
2. How Dynamic Imports Trigger Resource Requests at Runtime
Static imports usually participate in dependency loading when the current module loads:
import OrderDetail from './OrderDetail.vue'
Dynamic imports return a Promise when the code execution reaches the corresponding location:
const loadOrderDetail = () =>
import('./OrderDetail.vue')
Route-based lazy loading uses the latter form.
When a user opens the homepage, the order details module may not have been requested yet. Only when the user navigates to the corresponding route does the dynamic import begin loading the module.
If resource fetching or module loading fails, the Promise returned by this dynamic import enters a rejected state. The routing framework then receives the error, and the page might stay on the original route, enter an error boundary, or display a blank area, depending on the project's error handling code.
The timeline for this hypothetical scenario is as follows:
| Time Point | Code in Browser | Order Details File Requested? |
|---|---|---|
| Morning, opens homepage | Old entry script | No |
| Afternoon, project deployed | Old entry script still running in tab | No |
| After deployment, clicks order details | Old entry triggers dynamic import | Yes, requests old file |
| Manually refreshes page | Fetches new entry script | Requests new file afterwards |
The most easily misunderstood point is the third step. The project having deployed a new version does not mean the JavaScript in the old tab is automatically replaced with new code. As long as the page is not refreshed, the old entry script continues to run.
Dynamic imports solve the problem of initial bundle size and on-demand loading, but they also introduce a state that needs handling during deployment: a user might request an old module that hasn't been loaded yet while the old code is still running.
3. How the Deployment Process Turns Old Files into 404s
Assume the deployment process uses the method of clearing the static directory, then uploading the new build artifacts:
Clear current assets directory
→ Upload new index.html
→ Upload new JavaScript and CSS
→ Deployment complete
If the files from the old version are deleted immediately, the server ultimately only retains:
assets/index-8f20d.js
assets/OrderDetail-f92c7.js
When the old tab requests OrderDetail-a81f3.js, the server can no longer return that resource, resulting in a 404.
This deployment method solves the problem of old files continuously accumulating and directory content mixing. Its cost is cutting off the old page's access to historical build artifacts.
Two simplified approaches can be compared:
| Deployment Method | New Users | Already Open Old Pages | Maintenance Cost |
|---|---|---|---|
| Directly replace and delete old resources | Get new version | Old modules may return 404 | Simple directory, but higher risk during version switching |
| Keep hashed old resources for a period | Get new version | Still have a chance to load old modules | Occupies more storage, requires a cleanup strategy |
The idea of keeping old resources is not to make all historical versions permanently available, but to provide a transition period for old pages that are still running.
Another common approach is to deploy versions to independent directories and switch the current version via the entry point:
/releases/20260729-1400/
/releases/20260729-1730/
After the entry point switches, new visitors get the new version, while the old version's resources remain in their original directory. After the transition period ends, earlier versions are cleaned up.
This adds version directories, storage overhead, rollback, and cleanup tasks. The project must also confirm that resource paths in the HTML point to the correct version and cannot just create directories while letting different versions share resource addresses that are easily overwritten.
Whether the actual implementation uses object storage, a CDN, container images, or server directories should be based on the project's existing deployment platform. This article only explains the relationship between version replacement and the availability of old resources.
4. What Information in the Network Tab Can Distinguish Version Issues from Path Issues
When seeing Loading chunk failed or Failed to fetch dynamically imported module, the first step is to open the Network tab and find the failed request.
The simplified log for this case is:
Request URL:
https://static.example.com/assets/OrderDetail-a81f3.js
Status Code:
404 Not Found
Initiator:
index-3c71e.js
This record is from the stage where the browser is running the old entry script and triggers the dynamic import.
The three fields provide different clues:
| Field | What It Helps Determine |
|---|---|
| Request URL | Which file the browser actually requested |
| Status Code | Whether the file doesn't exist, a permission issue, or a service exception |
| Initiator | Which script triggered this request |
If the server currently only has OrderDetail-f92c7.js, and the request initiator is still index-3c71e.js, the likelihood of a mismatch between the old page and the new artifacts is high.
Other behaviors require different investigation directions:
| Request Behavior | Priority Check |
|---|---|
| Requests old hashed file and returns 404 | Whether old resources were deleted during deployment |
| All async files request to the wrong directory | Build base path and deployment path |
| Returns 200, but content is HTML | Server fallback rules or proxy configuration |
| Status code is 5xx | Static service, CDN, or origin server anomaly |
| Request is cancelled or hangs for a long time | Network conditions and client environment |
| File exists but browser refuses to execute | MIME type, CORS, or security policy |
Therefore, the client-side error text can only indicate that the dynamic module failed to load successfully; it cannot alone explain the specific cause.
If the wrong directory is still requested after a refresh, the problem is usually more than just an old version. At this point, you should continue to check the build path, proxy forwarding, and static service configuration, rather than repeatedly refreshing the page.
5. Why HTML and Hashed Resources Need Different Caching Strategies
When a page loads an old or new version, HTML and hashed resources bear different responsibilities.
The HTML usually stores the current entry file address:
<script
type="module"
src="/assets/index-8f20d.js"
></script>
Hashed JavaScript and CSS correspond to specific build content. When the content changes, the filename also changes.
Therefore, simplified caching goals can be written as:
| Resource | Caching Goal |
|---|---|
| HTML | Re-validate the current version before use |
| Hashed JS/CSS | Long-term reuse within the validity period |
| Config files without version identifiers | Set separately based on update frequency |
The following response headers are only for explaining the mechanism and do not mean all deployment platforms should copy them directly.
HTML can use:
Cache-Control: no-cache
Here, no-cache does not mean the browser cannot save the response at all. It means the cached content needs to be re-validated with the server before reuse. This way, when a user refreshes or revisits, they are more likely to get the current entry script address.
Static resources with content hashes can use:
Cache-Control: public, max-age=31536000, immutable
As long as the content corresponding to the same URL is not overwritten, a longer cache time can reduce repeated requests. When the content changes, a new filename is generated, and the HTML references the new address.
Two types of problems can arise when the cache configuration is reversed:
| Configuration Problem | Possible Result |
|---|---|
| HTML uses old cache directly for a long time | Users continuously get the old entry address |
| The same hashed resource URL is overwritten | Content in the cache is unstable with the deployed content |
| Static resources are not allowed to cache | Repeated downloads, losing the reuse value of hashed files |
| Old resources are deleted immediately | On-demand modules for already open pages may 404 |
Even if the HTML is re-validated every time, it cannot update the entry script in an already open tab. The caching strategy mainly affects refreshes and new visits. Whether an old page can still load modules still depends on whether the old resources are retained and whether the client provides a recovery process.
6. What Problems Can Client-Side Automatic Refresh Solve
Vite triggers the vite:preloadError event when a dynamic import fails to load. A project can listen for this event and read the original error.
Below is a simplified recovery example:
window.addEventListener(
'vite:preloadError',
event => {
event.preventDefault()
console.error(event.payload)
window.location.reload()
}
)
This code is in the client-side error recovery phase.
If the problem comes from an old page requesting a deleted old file, after the refresh, the browser may fetch the new HTML and new entry script, thus recovering to the current version.
event.preventDefault() prevents the error from continuing to be thrown in the default way. event.payload can be used to report the original exception, helping to distinguish between resource addresses, network failures, and other module loading issues.
This code is just a mechanism example; putting it directly into a production project has several problems:
- Temporary network failures could also trigger a refresh;
- If the build path is persistently wrong, it will still fail after a refresh;
- Without limiting the number of attempts, it could form a refresh loop;
- Unsaved forms and edited content from the user may be lost;
- A refresh alone cannot handle incompatibility between the old frontend and new APIs;
- Without error reporting before the automatic refresh, the problem may be hidden.
A more robust recovery process usually needs to include:
Log the error and current version
→ Determine if a recovery has already been attempted
→ Check if the page has unsaved content
→ Automatically refresh or prompt the user to refresh
→ Stop the loop and display an error page if it fails again
Page-side recovery solves how the user can continue operating after encountering an error. Retaining old resources on the deployment side solves whether the old page can complete its original loading. The two handle different stages and cannot replace each other.
7. Why Retaining Old Resources Still Cannot Handle All Version Conflicts
Assume the old order details script is retained, and the old tab can continue to load the page. But if the new version's backend API has deleted an old field, the old page might still fail when requesting data.
For example, the old version expects:
{
"orderStatus": "paid"
}
The new API only returns:
{
"status": "paid"
}
Static resources being loadable does not mean the old frontend and new API are still compatible.
Therefore, the deployment strategy must also consider:
- Whether the backend API allows simultaneous calls from old and new frontends;
- Whether data structure changes have a transition period;
- Whether permissions and routing rules are still compatible;
- Whether data submitted by the old page will be accepted by the new API;
- Whether the database and API can return to the corresponding state during a rollback.
The problem can be divided into two layers:
| Layer | Problem Solved |
|---|---|
| Retaining old build artifacts | Whether the old page can load old JavaScript |
| API compatibility and version governance | Whether the old code can continue communicating with the current service |
Retaining old chunks only handles the first layer.
If a project deploys very frequently, users might have pages open for hours or even days. How long old resources should be retained needs to be judged based on the average session length, storage costs, and API compatibility cycles; there is no universal number of days that can be directly applied.
8. How to Reproduce and Verify the Old Page Scenario After Deployment
This problem has a verification condition that is easy to miss: the test page must not be refreshed.
If you only open a new page for verification after deployment, the browser will fetch the new entry script from the start, naturally making it impossible to reproduce the process of an old page requesting an old chunk.
Pre-deployment verification can be performed with the following steps:
1. Deploy Version A
2. Open the homepage, but do not enter the order details
3. Keep the tab without refreshing
4. Deploy Version B, and modify dependencies related to order details
5. Go back to the old tab for Version A
6. Click on order details
7. Check the async resource requests and page recovery behavior
At a minimum, record the following during verification:
| Check Item | Result to Confirm |
|---|---|
| Filename requested by the old tab | Is it still the file from Version A |
| Whether the old resource exists | Returns 200 or 404 |
| Client recovery logic | Whether it reports, prompts, or refreshes |
| Entry file after refresh | Whether it switches to Version B |
| User's unsaved content | Whether it is preserved or explicitly prompted |
| Consecutive failures | Whether the automatic refresh loop stops |
You should also separately verify cases with cache hits and misses. If the CDN or browser cache still retains the old file, the problem might not appear temporarily, but the origin server file has already been deleted. After the cache expires, the old page may still fail.
A deployment check cannot just look at whether the build command succeeded. A successful build means the artifacts have been generated, but it cannot explain whether the old page is still usable during the version switch.
9. What Protections Are Still Missing from the Current Handling Approach
This article has focused on the mismatch between old pages and new build artifacts, but the same error text can also come from other causes.
First, incorrect resource base path configuration. When all dynamic files request to the wrong directory, a refresh usually won't recover; you need to check the build path and deployment directory.
Second, the server uniformly returns index.html for unknown static resources. The request status might be 200, but the browser receives HTML, and the module still cannot execute. You need to check the response content and MIME type.
Third, CDN deployment and invalidation have propagation times. Some nodes are already on the new version, while others still return old content, potentially forming short-term inconsistencies.
Fourth, a Service Worker might cache the HTML, entry script, or async modules. When a project uses a PWA, you also need to check the Service Worker's update, activation, and old cache cleanup processes.
Fifth, network failures and browser extensions can also block dynamic module requests. Only when old hashed files are requested and the problem appears concentrated after a deployment does it more closely match the characteristics of a version mismatch.
A project can establish a shorter investigation path:
Confirm the failed request URL
→ Check the status code and response content
→ Find the request initiator script
→ Compare with the current build artifacts
→ Check the deployment and caching strategy
→ Then decide to refresh, prompt, or fix the configuration
This process reduces the trial-and-error of directly refreshing, disabling lazy loading, or modifying build tools upon seeing the error. The final solution still depends on the project's deployment platform, caching layers, API compatibility, and user session length.
10. Summary
On the surface, this problem is a Loading chunk failed error when a user navigates to a lazy-loaded page, which recovers after a refresh. The old tab was continuously running the old entry script; when the new version was deployed, the old chunk was deleted; when the user later triggered a dynamic import, the old script continued to request the old file, causing the server to return a 404.
A project can handle this from three stages: the deployment side retains old build artifacts for a transition period; the caching side ensures HTML promptly confirms the current entry point and long-term reuses hashed resources; the client side logs errors and provides a limited recovery method when module loading fails. Each layer solves a different part of the process and also adds costs for storage cleanup, cache configuration, refresh protection, and error reporting.
These measures still cannot handle all problems. Incompatibility between the old frontend and new APIs, incorrect resource paths, CDN inconsistencies, Service Worker caching, and temporary network failures can all produce similar phenomena.
The next time you encounter a dynamic module loading failure, first confirm in the Network tab which file the browser requested, which entry script initiated it, and what the server returned. A refresh is just a recovery action; the difference between the failed request and the build artifacts is the main basis for judging whether the deployment pipeline is misaligned. The next article will discuss what information frontend error monitoring should record, focusing on what context needs to be preserved for resource loading errors, API errors, and code exceptions.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Great article, thanks for sharing.