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/index.ts file it will be bundled and automatically registered.
src/service-worker.tsor.jsis also valid, but see the section on type safety below
Inside the service worker
For the service worker to do anything useful, you will likely need to import some stuff:
$app/service-workerexportsselfwhich is justglobalThistyped asServiceWorkerGlobalScope(provided you follow these steps), so that yourfetchevents are typed correctly$app/envexportsversion, which is useful for creating deployment-scoped caches$app/manifestexportsimmutablebuild files, yourassets, and anyprerenderedcontent, allowing you to populate your caches
A typical service worker might look like this:
import { const self: ServiceWorkerGlobalScopeThe execution context of a service worker. This export exists to make it easier to
use service workers with the correct types, provided the importing module is governed
by a tsconfig.json that extends $app/tsconfig/service-worker.
self } from '$app/service-worker';
import { const version: stringThe value of config.version.name.
version } from '$app/env';
import { const immutable: {
path: string;
}[]
An array of { path: string } objects representing the files generated by Vite.
The path is relative to the base path, and is intended for use with cache.add(...) inside a service worker.
During development, this is an empty array.
immutable, const assets: {
path: import("$app/types").AssetPath;
}[]
An array of { path: AssetPath } objects representing the files in your static directory, or whatever directory is specified by config.files.assets.
The path is relative to the base path, and can be used with asset(...).
assets } from '$app/manifest';
import { function resolve<T extends RouteIdWithSearchOrHash | PathnameWithSearchOrHash>(...args: ResolveArgs<T>): ResolvedPathnameResolve a pathname by prefixing it with the base path, if any, or resolve a route ID by populating dynamic segments with parameters.
During server rendering, the base path is relative and depends on the page currently being rendered.
resolve } from '$app/paths';
// Create a unique cache name for this deployment
const const CACHE: stringCACHE = `cache-${const version: stringThe value of config.version.name.
version}`;
// `immutable`/`assets` paths from `$app/manifest` are relative to the
// base path, so resolve them to absolute pathnames that can be matched
// against `url.pathname` in the `fetch` handler
const const ASSETS: string[]ASSETS = [
...const immutable: {
path: string;
}[]
An array of { path: string } objects representing the files generated by Vite.
The path is relative to the base path, and is intended for use with cache.add(...) inside a service worker.
During development, this is an empty array.
immutable.Array<{ path: string; }>.map<string>(callbackfn: (value: {
path: string;
}, index: number, array: {
path: string;
}[]) => string, thisArg?: any): string[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((asset: {
path: string;
}
asset) => resolve<string>(pathname: string): ResolvedPathnameResolve a pathname by prefixing it with the base path, if any, or resolve a route ID by populating dynamic segments with parameters.
During server rendering, the base path is relative and depends on the page currently being rendered.
resolve(asset: {
path: string;
}
asset.path: stringpath)), // the Vite output
...const assets: {
path: import("$app/types").AssetPath;
}[]
An array of { path: AssetPath } objects representing the files in your static directory, or whatever directory is specified by config.files.assets.
The path is relative to the base path, and can be used with asset(...).
assets.Array<{ path: import("$app/types").AssetPath; }>.map<string>(callbackfn: (value: {
path: import("$app/types").AssetPath;
}, index: number, array: {
path: import("$app/types").AssetPath;
}[]) => string, thisArg?: any): string[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((asset: {
path: import("$app/types").AssetPath;
}
asset) => resolve<string>(pathname: string): ResolvedPathnameResolve a pathname by prefixing it with the base path, if any, or resolve a route ID by populating dynamic segments with parameters.
During server rendering, the base path is relative and depends on the page currently being rendered.
resolve(asset: {
path: import("$app/types").AssetPath;
}
asset.path: stringpath)) // everything in `static`
];
const self: ServiceWorkerGlobalScopeThe execution context of a service worker. This export exists to make it easier to
use service workers with the correct types, provided the importing module is governed
by a tsconfig.json that extends $app/tsconfig/service-worker.
self.ServiceWorkerGlobalScope.addEventListener<"install">(type: "install", listener: (this: ServiceWorkerGlobalScope, ev: ExtendableEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
addEventListener('install', (event: ExtendableEventevent) => {
// 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: CacheStorageAvailable only in secure contexts.
caches.CacheStorage.open(cacheName: string): Promise<Cache> (+1 overload)The open() method of the CacheStorage interface returns a Promise that resolves to the Cache object matching the cacheName.
open(const CACHE: stringCACHE);
await const cache: Cachecache.Cache.addAll(requests: Iterable<RequestInfo>): Promise<void> (+3 overloads)The addAll() method of the Cache interface takes an array of URLs, retrieves them, and adds the resulting response objects to the given cache. The request objects created during retrieval become keys to the stored response operations.
addAll(const ASSETS: string[]ASSETS);
}
event: ExtendableEventevent.ExtendableEvent.waitUntil(f: Promise<any>): voidThe ExtendableEvent.waitUntil() method tells the event dispatcher that work is ongoing. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete.
waitUntil(function (local function) addFilesToCache(): Promise<void>addFilesToCache());
});
const self: ServiceWorkerGlobalScopeThe execution context of a service worker. This export exists to make it easier to
use service workers with the correct types, provided the importing module is governed
by a tsconfig.json that extends $app/tsconfig/service-worker.
self.ServiceWorkerGlobalScope.addEventListener<"activate">(type: "activate", listener: (this: ServiceWorkerGlobalScope, ev: ExtendableEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
addEventListener('activate', (event: ExtendableEventevent) => {
// Remove previous cached data from disk
async function function (local function) deleteOldCaches(): Promise<void>deleteOldCaches() {
for (const const key: stringkey of await var caches: CacheStorageAvailable only in secure contexts.
caches.CacheStorage.keys(): Promise<string[]> (+1 overload)The keys() method of the CacheStorage interface returns a Promise that will resolve with an array containing strings corresponding to all of the named Cache objects tracked by the CacheStorage object in the order they were created. Use this method to iterate over a list of all Cache objects.
keys()) {
if (const key: stringkey !== const CACHE: stringCACHE) await var caches: CacheStorageAvailable only in secure contexts.
caches.CacheStorage.delete(cacheName: string): Promise<boolean> (+1 overload)The delete() method of the CacheStorage interface finds the Cache object matching the cacheName, and if found, deletes the Cache object and returns a Promise that resolves to true. If no Cache object is found, it resolves to false.
delete(const key: stringkey);
}
}
event: ExtendableEventevent.ExtendableEvent.waitUntil(f: Promise<any>): voidThe ExtendableEvent.waitUntil() method tells the event dispatcher that work is ongoing. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete.
waitUntil(function (local function) deleteOldCaches(): Promise<void>deleteOldCaches());
});
const self: ServiceWorkerGlobalScopeThe execution context of a service worker. This export exists to make it easier to
use service workers with the correct types, provided the importing module is governed
by a tsconfig.json that extends $app/tsconfig/service-worker.
self.ServiceWorkerGlobalScope.addEventListener<"fetch">(type: "fetch", listener: (this: ServiceWorkerGlobalScope, ev: FetchEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
addEventListener('fetch', (event: FetchEventevent) => {
// ignore POST requests etc
if (event: FetchEventevent.FetchEvent.request: RequestThe request read-only property of the FetchEvent interface returns the Request that triggered the event handler.
request.Request.method: stringThe method read-only property of the Request interface contains the request's method (GET, POST, etc.)
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) => URLThe URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.
URL class is a global reference for import { URL } from 'url'
https://nodejs.org/api/url.html#the-whatwg-url-api
URL(event: FetchEventevent.FetchEvent.request: RequestThe request read-only property of the FetchEvent interface returns the Request that triggered the event handler.
request.Request.url: stringThe url read-only property of the Request interface contains the URL of the request.
url);
const const cache: Cachecache = await var caches: CacheStorageAvailable only in secure contexts.
caches.CacheStorage.open(cacheName: string): Promise<Cache> (+1 overload)The open() method of the CacheStorage interface returns a Promise that resolves to the Cache object matching the cacheName.
open(const CACHE: stringCACHE);
// `immutable`/`assets` can always be served from the cache
if (const ASSETS: string[]ASSETS.Array<string>.includes(searchElement: string, fromIndex?: number): booleanDetermines whether an array includes a certain element, returning true or false as appropriate.
includes(const url: URLurl.URL.pathname: stringThe pathname property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character.
pathname)) {
const const response: Response | undefinedresponse = await const cache: Cachecache.Cache.match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined> (+1 overload)The match() method of the Cache interface returns a Promise that resolves to the Response associated with the first matching request in the Cache object. If no match is found, the Promise resolves to undefined.
match(const url: URLurl.URL.pathname: stringThe pathname property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character.
pathname);
if (const response: Response | undefinedresponse) {
return const response: Responseresponse;
}
}
// for everything else, try the network first...
try {
const const response: Responseresponse = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+2 overloads)fetch(event: FetchEventevent.FetchEvent.request: RequestThe request read-only property of the FetchEvent interface returns the Request that triggered the event handler.
request);
if (const response: Responseresponse.Response.status: numberThe status read-only property of the Response interface contains the HTTP status codes of the response.
status === 200 && !const response: Responseresponse.Response.headers: HeadersThe headers read-only property of the Response interface contains the Headers object associated with the response.
headers.Headers.get(name: string): string | null (+1 overload)The get() method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null.
get('cache-control')?.String.includes(searchString: string, position?: number): booleanReturns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
includes('no-store')) {
// ...and cache responses in the background for next time....
void const cache: Cachecache.Cache.put(request: RequestInfo | URL, response: Response): Promise<void> (+1 overload)The put() method of the Cache interface allows key/value pairs to be added to the current Cache object.
put(event: FetchEventevent.FetchEvent.request: RequestThe request read-only property of the FetchEvent interface returns the Request that triggered the event handler.
request, const response: Responseresponse.Response.clone(): Response (+1 overload)The clone() method of the Response interface creates a clone of a response object, identical in every way, but stored in a different variable.
clone());
}
return const response: Responseresponse;
} catch (function (local var) error: unknownerror) {
// ...otherwise fall back to previously cached data if it exists...
const const response: Response | undefinedresponse = await const cache: Cachecache.Cache.match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined> (+1 overload)The match() method of the Cache interface returns a Promise that resolves to the Response associated with the first matching request in the Cache object. If no match is found, the Promise resolves to undefined.
match(event: FetchEventevent.FetchEvent.request: RequestThe request read-only property of the FetchEvent interface returns the Request that triggered the event handler.
request);
if (const response: Response | undefinedresponse) {
return const response: Responseresponse;
}
// ...or throw the error
throw function (local var) error: unknownerror;
}
}
event: FetchEventevent.FetchEvent.respondWith(r: Response | PromiseLike<Response>): voidThe respondWith() method of FetchEvent prevents the browser's default fetch handling, and allows you to provide a promise for a Response yourself.
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.
Type safety
Service workers run in a different context to the rest of your app. As such, they needs different types. You should ensure that your project's root tsconfig.json excludes your service worker code...
{
"extends": "$app/tsconfig",
"include": ["src", "test"],
"exclude": ["src/service-worker"]
}...and that your src/service-worker/index.ts file sits alongside a separate tsconfig.json, which should set up the correct types by extending $app/tsconfig/service-worker:
{
"extends": "$app/tsconfig/service-worker"
}Manual registration
You can disable automatic registration if you need to register the service worker with your own logic. The default registration, which is injected into server-rendered HTML, looks something like this:
if ('serviceWorker' in var navigator: NavigatorThe Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
navigator) {
const const script_url: "./service-worker.js"script_url = './service-worker.js';
const const policy: anypolicy = module globalThisglobalThis?.var window: Window & typeof globalThisThe window property of a Window object points to the window object itself.
window?.trustedTypes?.createPolicy(
'sveltekit-trusted-url',
{ function createScriptURL(url: any): anycreateScriptURL(url: anyurl) { return url: anyurl; } }
);
const const sanitised: anysanitised = const policy: anypolicy?.createScriptURL(const script_url: "./service-worker.js"script_url) ?? const script_url: "./service-worker.js"script_url;
function addEventListener<"load">(type: "load", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)addEventListener('load', function () {
var navigator: NavigatorThe Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
navigator.Navigator.serviceWorker: ServiceWorkerContainerThe serviceWorker read-only property of the Navigator interface returns the ServiceWorkerContainer object for the associated document, which provides access to registration, removal, upgrade, and communication with the ServiceWorker.
Available only in secure contexts.
serviceWorker.ServiceWorkerContainer.register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>The register() method of the ServiceWorkerContainer interface creates or updates a ServiceWorkerRegistration for the given scope.
register(const sanitised: anysanitised, { RegistrationOptions.type?: WorkerType | undefinedtype: 'module' });
});
}The service worker is bundled for production, but not during development.
Updating the service worker
Browsers check for an updated service worker when a full-page navigation happens within its scope, and after functional events such as push and sync. Client-side navigations are neither, so navigating around your app will not by itself cause a new deployment's service worker to be picked up.
SvelteKit calls registration.update() only as part of error recovery — if a route module fails to load or a navigation results in an error status, and version polling detects that the app has been redeployed, the service worker is updated before SvelteKit falls back to a full-page navigation.
If you want new deployments to be picked up more eagerly, you can trigger an update check yourself — for example on every client-side navigation, in your root layout:
import { function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): voidA lifecycle function that runs the supplied callback when the current component mounts, and also whenever we navigate to a URL.
afterNavigate must be called during a component initialization. It remains active as long as the component is mounted.
afterNavigate } from '$app/navigation';
function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): voidA lifecycle function that runs the supplied callback when the current component mounts, and also whenever we navigate to a URL.
afterNavigate must be called during a component initialization. It remains active as long as the component is mounted.
afterNavigate(async () => {
if ('serviceWorker' in var navigator: NavigatorThe Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
navigator) {
const const registration: ServiceWorkerRegistration | undefinedregistration = await var navigator: NavigatorThe Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
navigator.Navigator.serviceWorker: ServiceWorkerContainerThe serviceWorker read-only property of the Navigator interface returns the ServiceWorkerContainer object for the associated document, which provides access to registration, removal, upgrade, and communication with the ServiceWorker.
Available only in secure contexts.
serviceWorker.ServiceWorkerContainer.getRegistration(clientURL?: string | URL): Promise<ServiceWorkerRegistration | undefined>The getRegistration() method of the ServiceWorkerContainer interface gets a ServiceWorkerRegistration object whose scope URL matches the provided client URL. The method returns a Promise that resolves to a ServiceWorkerRegistration or undefined.
getRegistration();
await const registration: ServiceWorkerRegistration | undefinedregistration?.ServiceWorkerRegistration.update(): Promise<ServiceWorkerRegistration>The update() method of the ServiceWorkerRegistration interface attempts to update the service worker. It fetches the worker's script URL, and if the new worker is not byte-by-byte identical to the current worker, it installs the new worker. The fetch of the worker bypasses any browser caches if the previous fetch occurred over 24 hours ago.
update();
}
});This will not cause the new service worker (if there is one) to take over the existing page immediately — instead, it will be installed in the background and take over as soon as the number of tabs managed by the existing service worker drops to zero.
Other solutions
SvelteKit's service worker implementation is designed to be easy to work with and is probably a good solution for most users. However, outside of SvelteKit, many PWA applications leverage the Workbox library. If you're used to using Workbox you may prefer Vite PWA plugin.
References
For more general information on service workers, we recommend the MDN web docs.
Edit this page on GitHub llms.txt