Vite chunkImportMap Explained: Handling Stale Chunks and Dynamic Import Failures
When deploying updates to Single Page Applications (SPAs) built with Vite and React, developers frequently encounter a common issue: open browser tabs throw a TypeError: Failed to fetch dynamically imported module error when trying to load lazy-loaded components. If you recently enabled Vite's build.chunkImportMap feature expecting it to automatically fix stale chunk loading issues across deployments, you might be surprised to find the error still occurring.
In this article, we will break down what build.chunkImportMap actually does, why open tabs fail to load missing chunks after a new deployment, and the best practices for handling dynamic import failures gracefully.
What Does Vite's build.chunkImportMap Actually Do?
Vite's build.chunkImportMap configuration option is designed to optimize asset delivery and cache management, not to dynamically update existing JavaScript runtimes in currently open browser tabs.
Specifically, chunkImportMap leverages native browser import maps to decouple entry points from deep dependency tree chunk hashes. This provides two major advantages:
- Prevents Cascading Cache Invalidation: Changing a small nested module won't alter the file hash of every parent bundle referencing it.
- Optimizes Preloading: Browsers can read the import map upfront to understand module locations efficiently without relying entirely on Vite's inline dynamic import polyfill or extra network round trips.
However, import maps are read and evaluated when the page initially loads. They do not magically fetch or update import maps in real-time for tabs that are already open in the background.
Why Open Tabs Still Fail After a Deployment
Consider the client-side lifecycle when you deploy code changes:
- A user opens your app (Version A). The browser downloads
index.html, the main bundle, and the initial import map, which referencesLazyPage-hashA.js. - You deploy Version B and clean out the build output directory (
dist).LazyPage-hashA.jsis deleted from the server and replaced withLazyPage-hashB.js. - The user clicks a button in their open Version A tab. The browser executes
import('./LazyPage-hashA.js')based on the JS code already loaded into browser memory. - The server returns a
404 Not Founderror becauseLazyPage-hashA.jsno longer exists. - The browser throws
TypeError: Failed to fetch dynamically imported module.
How to Fix Stale Chunk Import Errors
To eliminate missing chunk errors for users with open tabs, you should implement one (or both) of the following solutions:
1. Use Non-Destructive Deployments (Best Practice)
The cleanest architectural fix is to retain old static assets on your server or CDN for a grace period (e.g., 24 to 72 hours) rather than overwriting the dist folder completely on every build.
If you upload your builds to an S3 bucket, Cloudflare R2, or a CDN using hashed filenames, never delete old hashed JS chunks during deployment. Because hashed filenames are immutable, old tabs will keep fetching LazyPage-hashA.js without issues, while new users will load Version B's assets.
2. Listen for the vite:preloadError Event
Vite automatically fires a global event named vite:preloadError whenever a dynamic import or preloaded chunk fails to fetch (such as encountering a 404 error). You can listen for this event and trigger a graceful window reload so the tab fetches the updated index.html and latest assets.
window.addEventListener('vite:preloadError', (event) => {
// Force reload the page to pick up the latest deployment
window.location.reload()
})3. Wrap React.lazy with Retry Logic
If you want tighter control or want to prevent infinite reload loops if a network failure occurs, you can create a wrapper around React's lazy() function to safely retry component imports:
import { lazy } from 'react'
function lazyWithRetry(componentImport) {
return lazy(async () => {
const hasRefreshed = window.sessionStorage.getItem('retry_lazy_page')
try {
const component = await componentImport()
window.sessionStorage.getItem('retry_lazy_page') &&
window.sessionStorage.removeItem('retry_lazy_page')
return component
} catch (error) {
if (!hasRefreshed) {
window.sessionStorage.setItem('retry_lazy_page', 'true')
window.location.reload()
}
throw error
}
})
}
// Usage
const LazyPage = lazyWithRetry(() => import('./LazyPage.jsx'))Conclusion
Vite's build.chunkImportMap is a powerful feature for modern module bundling and caching efficiency, but it does not alter client-side runtime memory for tabs opened prior to a deployment. To prevent deployment-related chunk errors, preserve old hashed assets on your CDN or handle dynamic import failures gracefully using the vite:preloadError event.