Skip to main content

Service workers

Service workers act as proxy servers that handle network requests inside your app. This makes it possible to make your app work offline, but even if you don’t need offline support (or can’t realistically implement it because of the type of app you’re building), it’s often worth using service workers to speed up navigation by precaching your built JS and CSS.

In SvelteKit, if you have a src/service-worker.js file (or src/service-worker/index.js) it will be bundled and automatically registered. You can change the location of your service worker if you need to.

You can disable automatic registration if you need to register the service worker with your own logic or use another solution. The default registration looks something like this:

if ('serviceWorker' in var navigator: Navigatornavigator) {
	function addEventListener<"load">(type: "load", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)addEventListener('load', function () {
		var navigator: Navigatornavigator.Navigator.serviceWorker: ServiceWorkerContainer

Available only in secure contexts.

MDN Reference

serviceWorker
.ServiceWorkerContainer.register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>register('./path/to/service-worker.js');
}); }

Inside the service worker

Inside the service worker you have access to the $service-worker module, which provides you with the paths to all static assets, build files and prerendered pages. You’re also provided with an app version string, which you can use for creating a unique cache name, and the deployment’s base path. If your Vite config specifies define (used for global variable replacements), this will be applied to service workers as well as your server/client builds.

The following example caches the built app and any files in static eagerly, and caches all other requests as they happen. This would make each page work offline once visited.

/// <reference types="@sveltejs/kit" />
import { const build: string[]

An array of URL strings representing the files generated by Vite, suitable for caching with cache.addAll(build). During development, this is an empty array.

build
, const files: string[]

An array of URL strings representing the files in your static directory, or whatever directory is specified by config.kit.files.assets. You can customize which files are included from static directory using config.kit.serviceWorker.files

files
, const version: string

See config.kit.version. It’s useful for generating unique cache names inside your service worker, so that a later deployment of your app can invalidate old caches.

version
} from '$service-worker';
// Create a unique cache name for this deployment const const CACHE: stringCACHE = `cache-${const version: string

See config.kit.version. It’s useful for generating unique cache names inside your service worker, so that a later deployment of your app can invalidate old caches.

version
}`;
const const ASSETS: string[]ASSETS = [ ...const build: string[]

An array of URL strings representing the files generated by Vite, suitable for caching with cache.addAll(build). During development, this is an empty array.

build
, // the app itself
...const files: string[]

An array of URL strings representing the files in your static directory, or whatever directory is specified by config.kit.files.assets. You can customize which files are included from static directory using config.kit.serviceWorker.files

files
// everything in `static`
]; var self: Window & typeof globalThisself.function addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options’s capture.

When set to true, options’s capture prevents callback from being invoked when the event’s eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event’s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event’s eventPhase attribute value is AT_TARGET.

When set to true, options’s passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.

When set to true, options’s once indicates that the callback will only be invoked once after which the event listener will be removed.

If an AbortSignal is passed for options’s signal, then the event listener will be removed when signal is aborted.

The event listener is appended to target’s event listener list and is not appended if it has the same type, callback, and capture.

MDN Reference

addEventListener
('install', (event: Eventevent) => {
// Create a new cache and add all files to it async function function (local function) addFilesToCache(): Promise<void>addFilesToCache() { const const cache: Cachecache = await var caches: CacheStorage

Available only in secure contexts.

MDN Reference

caches
.CacheStorage.open(cacheName: string): Promise<Cache>open(const CACHE: stringCACHE);
await const cache: Cachecache.Cache.addAll(requests: Iterable<RequestInfo>): Promise<void> (+1 overload)addAll(const ASSETS: string[]ASSETS); } event: Eventevent.waitUntil(function (local function) addFilesToCache(): Promise<void>addFilesToCache()); }); var self: Window & typeof globalThisself.function addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options’s capture.

When set to true, options’s capture prevents callback from being invoked when the event’s eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event’s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event’s eventPhase attribute value is AT_TARGET.

When set to true, options’s passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.

When set to true, options’s once indicates that the callback will only be invoked once after which the event listener will be removed.

If an AbortSignal is passed for options’s signal, then the event listener will be removed when signal is aborted.

The event listener is appended to target’s event listener list and is not appended if it has the same type, callback, and capture.

MDN Reference

addEventListener
('activate', (event: Eventevent) => {
// Remove previous cached data from disk async function function (local function) deleteOldCaches(): Promise<void>deleteOldCaches() { for (const const key: stringkey of await var caches: CacheStorage

Available only in secure contexts.

MDN Reference

caches
.CacheStorage.keys(): Promise<string[]>keys()) {
if (const key: stringkey !== const CACHE: stringCACHE) await var caches: CacheStorage

Available only in secure contexts.

MDN Reference

caches
.CacheStorage.delete(cacheName: string): Promise<boolean>delete(const key: stringkey);
} } event: Eventevent.waitUntil(function (local function) deleteOldCaches(): Promise<void>deleteOldCaches()); }); var self: Window & typeof globalThisself.function addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options’s capture.

When set to true, options’s capture prevents callback from being invoked when the event’s eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event’s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event’s eventPhase attribute value is AT_TARGET.

When set to true, options’s passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.

When set to true, options’s once indicates that the callback will only be invoked once after which the event listener will be removed.

If an AbortSignal is passed for options’s signal, then the event listener will be removed when signal is aborted.

The event listener is appended to target’s event listener list and is not appended if it has the same type, callback, and capture.

MDN Reference

addEventListener
('fetch', (event: Eventevent) => {
// ignore POST requests etc if (event: Eventevent.request.method !== 'GET') return; async function function (local function) respond(): Promise<Response>respond() { const const url: URLurl = new var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface represents an object providing static methods used for creating object URLs.

MDN Reference

URL class is a global reference for require('url').URL https://nodejs.org/api/url.html#the-whatwg-url-api

@sincev10.0.0
URL
(event: Eventevent.request.url);
const const cache: Cachecache = await var caches: CacheStorage

Available only in secure contexts.

MDN Reference

caches
.CacheStorage.open(cacheName: string): Promise<Cache>open(const CACHE: stringCACHE);
// `build`/`files` can always be served from the cache if (const ASSETS: string[]ASSETS.Array<string>.includes(searchElement: string, fromIndex?: number): boolean

Determines whether an array includes a certain element, returning true or false as appropriate.

@paramsearchElement The element to search for.
@paramfromIndex The position in this array at which to begin searching for searchElement.
includes
(const url: URLurl.URL.pathname: stringpathname)) {
const const response: Response | undefinedresponse = await const cache: Cachecache.Cache.match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>match(const url: URLurl.URL.pathname: stringpathname); if (const response: Response | undefinedresponse) { return const response: Responseresponse; } } // for everything else, try the network first, but // fall back to the cache if we're offline try { const const response: Responseresponse = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)fetch(event: Eventevent.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 (!(const response: Responseresponse instanceof
var Response: {
    new (body?: BodyInit | null, init?: ResponseInit): Response;
    prototype: Response;
    error(): Response;
    json(data: any, init?: ResponseInit): Response;
    redirect(url: string | URL, status?: number): Response;
}

This Fetch API interface represents the response to a request.

MDN Reference

Response
)) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('invalid response from fetch');
} if (const response: Responseresponse.Response.status: numberstatus === 200) { const cache: Cachecache.Cache.put(request: RequestInfo | URL, response: Response): Promise<void>put(event: Eventevent.request, const response: Responseresponse.Response.clone(): Responseclone()); } return const response: Responseresponse; } catch (function (local var) err: unknownerr) { const const response: Response | undefinedresponse = await const cache: Cachecache.Cache.match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>match(event: Eventevent.request); if (const response: Response | undefinedresponse) { return const response: Responseresponse; } // if there's no cache, then just error out // as there is nothing we can do to respond to this request throw function (local var) err: unknownerr; } } event: Eventevent.respondWith(function (local function) respond(): Promise<Response>respond()); });

Be careful when caching! In some cases, stale data might be worse than data that’s unavailable while offline. Since browsers will empty caches if they get too full, you should also be careful about caching large assets like video files.

During development

The service worker is bundled for production, but not during development. For that reason, only browsers that support modules in service workers will be able to use them at dev time. If you are manually registering your service worker, you will need to pass the { type: 'module' } option in development:

import { const dev: boolean

Whether the dev server is running. This is not guaranteed to correspond to NODE_ENV or MODE.

dev
} from '$app/environment';
var navigator: Navigatornavigator.Navigator.serviceWorker: ServiceWorkerContainer

Available only in secure contexts.

MDN Reference

serviceWorker
.ServiceWorkerContainer.register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>register('/service-worker.js', {
RegistrationOptions.type?: WorkerType | undefinedtype: const dev: boolean

Whether the dev server is running. This is not guaranteed to correspond to NODE_ENV or MODE.

dev
? 'module' : 'classic'
});

build and prerendered are empty arrays during development

Type safety

Setting up proper types for service workers requires some manual setup. Inside your service-worker.js, add the following to the top of your file:

/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self));
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

const sw = self as unknown as ServiceWorkerGlobalScope;

This disables access to DOM typings like HTMLElement which are not available inside a service worker and instantiates the correct globals. The reassignment of self to sw allows you to type cast it in the process (there are a couple of ways to do this, but this is the easiest that requires no additional files). Use sw instead of self in the rest of the file. The reference to the SvelteKit types ensures that the $service-worker import has proper type definitions. If you import $env/static/public you either have to // @ts-ignore the import or add /// <reference types="../.svelte-kit/ambient.d.ts" /> to the reference types.

Other solutions

SvelteKit’s service worker implementation is deliberately low-level. If you need a more full-fledged but also more opinionated solution, we recommend looking at solutions like Vite PWA plugin, which uses Workbox. For more general information on service workers, we recommend the MDN web docs.

Edit this page on GitHub