Skip to main content
Service workers act as proxy servers that handle network requests inside your app. This makes it possible to make your app work offline and speed up navigation by precaching built assets.

Setup

If you have a src/service-worker.js file (or src/service-worker/index.js), SvelteKit will bundle and automatically register it.
Service workers are bundled for production but not during development.

Automatic registration

The default registration looks like this:
if ('serviceWorker' in navigator) {
	addEventListener('load', function () {
		navigator.serviceWorker.register('./path/to/service-worker.js');
	});
}

Custom registration

You can disable automatic registration and register manually:
import { dev } from '$app/environment';

navigator.serviceWorker.register('/service-worker.js', {
	type: dev ? 'module' : 'classic'
});
During development, you must pass type: 'module' because only browsers that support modules in service workers can use them at dev time.

Inside the service worker

The $service-worker module provides access to:
build
string[]
Paths to all built app files
files
string[]
Paths to all files in the static directory
prerendered
string[]
Paths to all prerendered pages
version
string
A unique app version string for creating cache names
base
string
The deployment’s base path

Complete example

This example caches the built app and static files eagerly, then caches other requests as they happen:
src/service-worker.js
// Type safety references
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
/// <reference types="@sveltejs/kit" />
/// <reference types="../.svelte-kit/ambient.d.ts" />

import { build, files, version } from '$service-worker';

const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self));

// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;

const ASSETS = [
	...build, // the app itself
	...files  // everything in `static`
];

self.addEventListener('install', (event) => {
	// Create a new cache and add all files to it
	async function addFilesToCache() {
		const cache = await caches.open(CACHE);
		await cache.addAll(ASSETS);
	}

	event.waitUntil(addFilesToCache());
});

self.addEventListener('activate', (event) => {
	// Remove previous cached data from disk
	async function deleteOldCaches() {
		for (const key of await caches.keys()) {
			if (key !== CACHE) await caches.delete(key);
		}
	}

	event.waitUntil(deleteOldCaches());
});

self.addEventListener('fetch', (event) => {
	// ignore POST requests etc
	if (event.request.method !== 'GET') return;

	async function respond() {
		const url = new URL(event.request.url);
		const cache = await caches.open(CACHE);

		// `build`/`files` can always be served from the cache
		if (ASSETS.includes(url.pathname)) {
			const response = await cache.match(url.pathname);

			if (response) {
				return response;
			}
		}

		// for everything else, try the network first, but
		// fall back to the cache if we're offline
		try {
			const response = await fetch(event.request);

			// if we're offline, fetch can return a value that is not a Response
			// instead of throwing - and we can't pass this non-Response to respondWith
			if (!(response instanceof Response)) {
				throw new Error('invalid response from fetch');
			}

			if (response.status === 200) {
				cache.put(event.request, response.clone());
			}

			return response;
		} catch (err) {
			const response = await cache.match(event.request);

			if (response) {
				return response;
			}

			// if there's no cache, then just error out
			// as there is nothing we can do to respond to this request
			throw err;
		}
	}

	event.respondWith(respond());
});
1

Install event

Opens a cache and adds all static assets to it
2

Activate event

Removes old caches from previous deployments
3

Fetch event

Serves cached assets immediately, network resources with cache fallback

Caching strategies

Check the cache first, fall back to network. Best for static assets.
const response = await cache.match(request) || await fetch(request);
Try network first, fall back to cache if offline. Best for dynamic content.
try {
  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
} catch (err) {
  return await cache.match(request);
}
Return cached response immediately while fetching fresh data in background.
const cached = await cache.match(request);
const fresh = fetch(request).then(r => {
  cache.put(request, r.clone());
  return r;
});
return cached || fresh;
Be careful when caching! In some cases, stale data might be worse than no data. Browsers will also empty caches if they get too full.

Development considerations

During development:
  • Service workers are not bundled
  • build and prerendered are empty arrays
  • Only browsers that support ES modules in service workers will work

Testing

1

Build your app

npm run build
2

Preview the build

npm run preview
3

Test offline

Open DevTools → Network tab → Enable “Offline” mode
4

Verify caching

Open DevTools → Application tab → Service Workers / Cache Storage

Alternative solutions

SvelteKit’s service worker implementation is simple and effective, but you might prefer:

Learn more

MDN Web Docs: Using Service Workers