Skew Protection

Handle stale client bundles gracefully after deployments

After a deployment, users with open tabs still have old JavaScript bundles. When they navigate, dynamic imports reference chunk URLs that no longer exist on the server, causing 404s, broken UI, or blank pages.

One has built-in skew protection that detects these failures and recovers automatically.

Configuration

vite.config.ts

import { one } from 'one/vite'
export default {
plugins: [
one({
web: {
skewProtection: true // true (default) | 'proactive' | false
}
})
]
}

Error Recovery (default)

With skewProtection: true (the default), One detects chunk load failures and automatically reloads the page. This covers two failure paths:

  • Vite preload errors — caught via the vite:preloadError event when Vite’s module preloader fails to fetch a chunk
  • Dynamic import errors — caught when One’s own dynamicImport() calls fail with browser-specific chunk load error messages (Chrome, Firefox, Safari)

When a failure is detected, the page reloads so the browser fetches fresh HTML with the new asset URLs. A sessionStorage guard prevents infinite reload loops — if a reload already happened within the last 10 seconds, the guard stops further reloads.

No configuration needed. This is always on unless you explicitly set skewProtection: false.

Proactive Mode

vite.config.ts

one({
web: {
skewProtection: 'proactive'
}
})

Proactive mode adds version polling on top of error recovery. Instead of waiting for a chunk to fail, the client periodically checks whether a new version has been deployed and forces a full-page navigation on the next link click.

How it works

  1. At build time, One emits a version.json file containing the build’s cache key
  2. The client polls version.json every 2 minutes with cache-busting headers
  3. When the polled version doesn’t match the running bundle’s version, the app is marked as stale
  4. On the next client-side navigation, instead of doing an SPA transition, One does a full window.location.href navigation so the server returns fresh HTML with new assets

This is similar to how SvelteKit handles version skew. The user never sees a broken page — they just get a slightly slower (full-page) navigation that loads the new deployment.

Events

When a version mismatch is detected, One dispatches a one-version-update custom event on window:

window.addEventListener('one-version-update', (e) => {
console.log('new version available:', e.detail.version)
// show a toast, banner, etc.
})

You can use this to show users a “new version available” banner with a manual refresh button instead of waiting for the next navigation.

Window flag

The window.__oneVersionStale flag is set to true when a version mismatch is detected. You can check this from anywhere in your app:

if (window.__oneVersionStale) {
// app has a newer deployment available
}

Disabling

To turn off skew protection entirely:

vite.config.ts

one({
web: {
skewProtection: false
}
})

This disables both the error recovery reload and the proactive polling. Chunk load failures will be silently swallowed as they were before this feature existed.

CDN Edge Caching

With skew protection enabled, One automatically sets cache headers that allow CDN edge caching for SSG and SPA pages:

Terminal

cache-control: public, s-maxage=60, stale-while-revalidate=120

This means:

  • CDN caches pages for 60 seconds — subsequent requests are served from edge, cutting TTFB to ~50ms
  • Stale-while-revalidate for 120 seconds — even after 60s, the CDN serves stale content while fetching fresh in the background
  • SSR pages are not cached — they return no-cache since content is dynamic per request

Why this is safe

Previously, CDN caching HTML was risky: after a deploy, cached HTML references old JS bundles that may not exist, causing 404s and broken pages.

Skew protection solves this. If a cached page tries to load a missing chunk:

  1. The chunk request 404s
  2. Skew protection detects the failure via vite:preloadError
  3. The page auto-reloads, fetching fresh HTML from the origin
  4. User sees the new deployment

The worst case is a brief flash on reload — much better than a broken page.

Overriding cache headers

If you need custom cache behavior, set headers in your loader or middleware:

import { setResponseHeaders } from 'one'
export function loader() {
// cache for 5 minutes at the edge
setResponseHeaders({
'cache-control': 'public, s-maxage=300'
})
return { data: '...' }
}

Custom headers take precedence — One only sets defaults when no cache-control header is present.

Edit this page on GitHub.