How OpenSEO Handles Automatic Chunk Reload Detection for Deployed Applications
OpenSEO uses Vite's vite:preloadError event to catch failed dynamic imports and automatically reload the page when stale chunks are detected after a deployment.
Automatic chunk reload detection ensures users with old browser tabs never encounter broken functionality when a new version of your app goes live. The every-app/open-seo repository implements this pattern to solve the common problem of hash-mismatched JavaScript chunks after a Vite build.
The Problem: Stale Chunks After Deployment
When Vite rebuilds an application, it generates content-hashed filenames for code-split chunks. A user who loaded your app before the deploy holds references to the old chunk hashes. If they navigate to a route that triggers a dynamic import, the browser requests a chunk that no longer exists on the server. Without intervention, this produces a silent failure or a blank screen.
How OpenSEO Detects Chunk Failures
The detection mechanism in [src/router.tsx](https://github.com/every-app/open-seo/blob/main/src/router.tsx) leverages a Vite-specific browser event that fires exclusively in production builds.
The vite:preloadError Event
Vite emits this custom event when a dynamic import() call fails due to a network error or module resolution failure. OpenSEO registers a global listener immediately upon client initialization—before React hydration completes—to intercept these errors as early as possible.
The implementation follows four steps:
- Register early – Add the listener as soon as
windowis available - Rate-limit reloads – Check
sessionStoragefor a recent reload timestamp - Record the attempt – Store the current time if the cooldown has passed
- Execute the reload – Prevent default handling and call
window.location.reload()
Implementation in src/router.tsx
The complete logic fits in a single event listener registration:
// src/router.tsx – automatic chunk-reload detection
if (typeof window !== "undefined") {
window.addEventListener("vite:preloadError", (event) => {
try {
const reloadedAt = Number(
window.sessionStorage.getItem("vite-preload-error-reloaded-at") ?? 0,
);
// Allow at most one reload per 30 seconds
if (Date.now() - reloadedAt < 30_000) return;
window.sessionStorage.setItem(
"vite-preload-error-reloaded-at",
String(Date.now()),
);
} catch {
// If sessionStorage is unavailable, skip the reload to avoid loops
return;
}
event.preventDefault();
window.location.reload();
});
}
Rate-Limiting Prevents Reload Loops
The 30-second cooldown stored under the vite-preload-error-reloaded-at key protects against edge cases where:
- The server serves inconsistent builds
- A browser extension blocks the new chunk
- Network conditions cause repeated failures
Without this guard, a persistent error condition would trap users in an infinite reload cycle.
Graceful Degradation
The try/catch block ensures the handler fails silently if sessionStorage is unavailable. This accommodates:
- Private browsing modes with restricted storage
- Security policies that disable
StorageAPIs - Embedded contexts like WebViews with limited permissions
Key Differences: Development vs. Production
| Environment | vite:preloadError Behavior |
|---|---|
| Development | Event does not fire; Vite serves files from memory without hashing |
| Production | Event fires on chunk load failures; enables automatic recovery |
This distinction means the automatic chunk reload detection only activates in built applications. Your development workflow remains unaffected.
Where This Pattern Fits in Your Architecture
OpenSEO places this logic in src/router.tsx because:
- It loads early in the application lifecycle
- It executes before route-based code splitting begins
- It remains decoupled from individual component implementations
You can adopt the same pattern in any Vite-based React, Vue, or Svelte application by adding the listener in your entry point or router initialization module.
Summary
- OpenSEO catches stale chunk errors using Vite's
vite:preloadErrorevent - The handler in
src/router.tsximplements 30-second rate limiting viasessionStorage - Reloads execute only when sufficient time has passed, preventing infinite loops
- The mechanism is production-only and gracefully handles storage restrictions
- Users automatically receive the latest deployment without manual intervention
Frequently Asked Questions
What triggers the vite:preloadError event?
Vite emits this event when a dynamic import fails to load, typically because the requested chunk filename no longer exists on the server after a new deployment. This occurs when chunk hashes change between builds and a client with cached HTML references the old files.
Why does OpenSEO use sessionStorage instead of localStorage?
sessionStorage resets when the tab closes, ensuring a fresh reload opportunity on the next visit. localStorage would persist the timestamp across sessions, potentially blocking legitimate reloads hours after a transient error occurred.
Can this pattern cause data loss for users?
A page reload does discard unsaved client-side state. However, the alternative—leaving users on a broken application with failed chunks—provides a worse experience. For critical user input, implement auto-save to localStorage or a backend before navigation triggers dynamic imports.
Does this work with other bundlers like webpack or Rollup?
No. The vite:preloadError event is specific to Vite's runtime. webpack offers similar functionality through the __webpack_chunk_load__ error hook, but the implementation differs. You would need to adapt the detection logic to your bundler's chunk loading API.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →