Skip to content

Update Flow

Deploying a new build means your service worker file changes — at minimum, version in $service-worker changes, causing `cache-\${version}` to be a new string. The browser detects this, downloads the new SW, and enters the update lifecycle described below.

Built-in SW (src/service-worker.js): SvelteKit registers it automatically during build. In development the type is 'module'; in production it is 'classic'. You never need to call navigator.serviceWorker.register() yourself.

@vite-pwa/sveltekit: The plugin handles registration via the registerType option. The virtual:pwa-register module (and its Svelte-specific variant virtual:pwa-register/svelte) exposes Svelte stores you can use in any component.

sequenceDiagram
  participant B as Browser
  participant OSW as Old SW (active)
  participant NSW as New SW (waiting)
  participant P as Page

  B->>NSW: Download + parse new sw.js
  NSW->>NSW: install event (precache new assets)
  NSW-->>OSW: waiting — old SW still controls tabs
  P->>P: updatefound event fires
  note over P: show reload banner
  P->>NSW: postMessage SKIP_WAITING
  NSW->>NSW: skipWaiting() — activates immediately
  NSW->>B: clients.claim() — takes over open tabs
  P->>P: reload
Service worker update sequence

Option A: autoUpdate with @vite-pwa/sveltekit

Section titled “Option A: autoUpdate with @vite-pwa/sveltekit”
vite.config.ts
SvelteKitPWA({
registerType: 'autoUpdate',
})

The new SW activates the moment its install phase completes — no user prompt. Best for apps where asset URLs are fingerprinted (hashed), so old and new assets cannot collide.

Option B: prompt with virtual:pwa-register/svelte

Section titled “Option B: prompt with virtual:pwa-register/svelte”
vite.config.ts
SvelteKitPWA({
registerType: 'prompt',
})

The new SW waits. Your Svelte component uses useRegisterSW from virtual:pwa-register/svelte to show a banner when an update is ready:

src/lib/ReloadPrompt.svelte
<script lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/svelte';
const { needRefresh, offlineReady, updateServiceWorker } = useRegisterSW({
onRegistered(r) {
console.log('SW registered:', r);
},
onRegisterError(error) {
console.error('SW registration error', error);
},
});
function close() {
offlineReady.set(false);
needRefresh.set(false);
}
$: toast = $offlineReady || $needRefresh;
</script>
{#if toast}
<div class="pwa-toast" role="alert">
{#if $offlineReady}
<span>App ready to work offline.</span>
{:else}
<span>New version available.</span>
{/if}
{#if $needRefresh}
<button on:click={() => updateServiceWorker(true)}>Reload</button>
{/if}
<button on:click={close}>Close</button>
</div>
{/if}

updateServiceWorker(true) sends SKIP_WAITING to the waiting SW and reloads the page once it activates.

Option C: manual updatefound / statechange (built-in SW)

Section titled “Option C: manual updatefound / statechange (built-in SW)”

When you use src/service-worker.js without @vite-pwa/sveltekit, wire the update detection yourself:

// src/app.ts (or any module that runs on every page)
async function registerAndWatch() {
if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register(
'/service-worker.js',
{ type: import.meta.env.DEV ? 'module' : 'classic' }
);
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateBanner(registration);
}
});
});
}
function showUpdateBanner(registration) {
const banner = document.createElement('div');
banner.textContent = 'New version available. ';
const btn = document.createElement('button');
btn.textContent = 'Reload';
btn.onclick = () => {
registration.waiting?.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
};
banner.appendChild(btn);
document.body.prepend(banner);
}
registerAndWatch();

Your service worker must listen for that message:

src/service-worker.js
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

SvelteKit apps are often SPAs — the user may stay on the same page for hours. Trigger a manual update check on an interval:

registration.addEventListener('updatefound', () => { /* … */ });
setInterval(() => registration.update(), 60 * 60 * 1000); // every hour
OptionBenefitCost
Let a Svelte reactive statement ($:) watch update state from a storeUI updates instantly, declaratively, no manual DOM updatesRequires understanding Svelte’s reactivity model well, or triggers fire at the wrong time
Poll registration.update() manually on an intervalClear, explicit control over the check timingAdds unnecessary network requests if the interval is set too aggressively
  • Not fully handling the updatefound event, so the UI never learns that a new service worker version is installing in the background.
  • Reloading the page the instant state changes without asking the user first, wiping out work in progress in a form.
  • Not debouncing the update check, firing registration.update() far more often than necessary on every navigation within the app.

The Vite PWA ecosystem for SvelteKit uses the virtual:pwa-register/svelte store to surface needRefresh so the UI can show a toast, the same standard pattern used on the React/Vue side, and Elk shows a prompt for the user to click reload themselves rather than force-reloading while someone is scrolling through their timeline.

What does registerType: "autoUpdate" do when a new SW is installed?
Which virtual module exposes needRefresh and updateServiceWorker as Svelte stores?
In the manual update pattern, what message type does the page send to the waiting SW?
Why is it important to keep skipWaiting() user-driven rather than calling it unconditionally in install?