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.
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.
src/service-worker
// Disables access to DOM typings like `HTMLElement` which are not available
// inside a service worker and instantiates the correct globals
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />// Ensures that the `$service-worker` import has proper type definitions/// <reference types="@sveltejs/kit" />// Only necessary if you have an import from `$env/static/public`/// <reference types="../.svelte-kit/ambient.d.ts" />import{build(alias)constbuild:string[]importbuild
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.
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
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.
reference}from'$service-worker';// This gives `self` the correct typesconstselfconstself:ServiceWorkerGlobalScope=/**@type{ServiceWorkerGlobalScope}*/(/**@type{unknown}*/(globalThismoduleglobalThis.selfvarself:Window&typeofglobalThis
The Window.self read-only property returns the window itself, as a WindowProxy. It can be used with dot notation on a window object (that is, window.self) or standalone (self). The advantage of the standalone notation is that a similar notation exists for non-window contexts, such as in Web Workers. By using self, you can refer to the global scope in a way that will work not only in a window context (self will resolve to window.self) but also in a worker context (self will then resolve to WorkerGlobalScope.self).
The self read-only property of the WorkerGlobalScope interface returns a reference to the WorkerGlobalScope itself. Most of the time it is a specific scope like DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, or ServiceWorkerGlobalScope.
));// Create a unique cache name for this deploymentconstCACHEconstCACHE:string=`cache-${version(alias)constversion:stringimportversion
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.
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.
reference,// the app itself...files(alias)constfiles:string[]importfiles
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
reference// everything in `static`];selfconstself:ServiceWorkerGlobalScope.addEventListener(method)ServiceWorkerGlobalScope.addEventListener<"install">(type:"install",listener:(this:ServiceWorkerGlobalScope,ev:ExtendableEvent)=>any,options?:boolean|AddEventListenerOptions):void(+1overload)
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
('install',(event(parameter)event:ExtendableEvent)=>{ // Create a new cache and add all files to itasyncfunctionaddFilesToCache(localfunction)addFilesToCache():Promise<void>(){constcacheconstcache:Cache=awaitcachesvarcaches:CacheStorage
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.
The 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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
('activate',(event(parameter)event:ExtendableEvent)=>{ // Remove previous cached data from diskasyncfunctiondeleteOldCaches(localfunction)deleteOldCaches():Promise<void>(){for(constkeyconstkey:stringofawaitcachesvarcaches:CacheStorage
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.
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.
The 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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The 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.
(CACHEconstCACHE:string); // `build`/`files` can always be served from the cacheif(ASSETSconstASSETS:string[].includes(method)Array<string>.includes(searchElement:string,fromIndex?:number):boolean
Determines whether an array includes a certain element, returning true or false as appropriate.
searchElement
The element to search for.
fromIndex
The position in this array at which to begin searching for searchElement.
The 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.
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.
The 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.
);if(responseconstresponse:Response|undefined){returnresponseconstresponse:Response;}} // for everything else, try the network first, but // fall back to the cache if we're offlinetry{constresponseconstresponse:Response=awaitfetchfunctionfetch(input:string|URL|Request,init?:RequestInit):Promise<Response>(+2overloads)
); // 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 respondWithif(!(responseconstresponse:ResponseinstanceofResponsevarResponse:{new(body?:BodyInit|null,init?:ResponseInit):Response;prototype:Response;error():Response;json(data:any,init?:ResponseInit):Response;redirect(url:string|URL,status?:number):Response;}
The Response interface of the Fetch API represents the response to a request.
)){thrownewErrorvarError:ErrorConstructornew(message?:string,options?:ErrorOptions)=>Error(+1overload)('invalid response from fetch');}if(responseconstresponse:Response.status(property)Response.status:number
The status read-only property of the Response interface contains the HTTP status codes of the response.
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.
Returns 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.
searchString
search string
position
If position is undefined, 0 is assumed, so as to search all of the String.
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.
);if(responseconstresponse:Response|undefined){returnresponseconstresponse:Response;} // if there's no cache, then just error out // as there is nothing we can do to respond to this requestthrowerr(localvar)err:unknown;}}event(parameter)event:FetchEvent.respondWith(method)FetchEvent.respondWith(r:Response|PromiseLike<Response>):void
The respondWith() method of FetchEvent prevents the browser's default fetch handling, and allows you to provide a promise for a Response yourself.
// Disables access to DOM typings like `HTMLElement` which are not available
// inside a service worker and instantiates the correct globals
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />// Ensures that the `$service-worker` import has proper type definitions/// <reference types="@sveltejs/kit" />// Only necessary if you have an import from `$env/static/public`/// <reference types="../.svelte-kit/ambient.d.ts" />import{build(alias)constbuild:string[]importbuild
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.
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
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.
reference}from'$service-worker';// This gives `self` the correct typesconstselfconstself:ServiceWorkerGlobalScope=globalThismoduleglobalThis.selfvarself:Window&typeofglobalThis
The Window.self read-only property returns the window itself, as a WindowProxy. It can be used with dot notation on a window object (that is, window.self) or standalone (self). The advantage of the standalone notation is that a similar notation exists for non-window contexts, such as in Web Workers. By using self, you can refer to the global scope in a way that will work not only in a window context (self will resolve to window.self) but also in a worker context (self will then resolve to WorkerGlobalScope.self).
The self read-only property of the WorkerGlobalScope interface returns a reference to the WorkerGlobalScope itself. Most of the time it is a specific scope like DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, or ServiceWorkerGlobalScope.
The ServiceWorkerGlobalScope interface of the Service Worker API represents the global execution context of a service worker.
Available only in secure contexts.
;// Create a unique cache name for this deploymentconstCACHEconstCACHE:string=`cache-${version(alias)constversion:stringimportversion
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.
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.
reference,// the app itself...files(alias)constfiles:string[]importfiles
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
reference// everything in `static`];selfconstself:ServiceWorkerGlobalScope.addEventListener(method)ServiceWorkerGlobalScope.addEventListener<"install">(type:"install",listener:(this:ServiceWorkerGlobalScope,ev:ExtendableEvent)=>any,options?:boolean|AddEventListenerOptions):void(+1overload)
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
('install',(event(parameter)event:ExtendableEvent)=>{ // Create a new cache and add all files to itasyncfunctionaddFilesToCache(localfunction)addFilesToCache():Promise<void>(){constcacheconstcache:Cache=awaitcachesvarcaches:CacheStorage
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.
The 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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
('activate',(event(parameter)event:ExtendableEvent)=>{ // Remove previous cached data from diskasyncfunctiondeleteOldCaches(localfunction)deleteOldCaches():Promise<void>(){for(constkeyconstkey:stringofawaitcachesvarcaches:CacheStorage
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.
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.
The 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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The 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.
(CACHEconstCACHE:string); // `build`/`files` can always be served from the cacheif(ASSETSconstASSETS:string[].includes(method)Array<string>.includes(searchElement:string,fromIndex?:number):boolean
Determines whether an array includes a certain element, returning true or false as appropriate.
searchElement
The element to search for.
fromIndex
The position in this array at which to begin searching for searchElement.
The 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.
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.
The 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.
);if(responseconstresponse:Response|undefined){returnresponseconstresponse:Response;}} // for everything else, try the network first, but // fall back to the cache if we're offlinetry{constresponseconstresponse:Response=awaitfetchfunctionfetch(input:string|URL|Request,init?:RequestInit):Promise<Response>(+2overloads)
); // 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 respondWithif(!(responseconstresponse:ResponseinstanceofResponsevarResponse:{new(body?:BodyInit|null,init?:ResponseInit):Response;prototype:Response;error():Response;json(data:any,init?:ResponseInit):Response;redirect(url:string|URL,status?:number):Response;}
The Response interface of the Fetch API represents the response to a request.
)){thrownewErrorvarError:ErrorConstructornew(message?:string,options?:ErrorOptions)=>Error(+1overload)('invalid response from fetch');}if(responseconstresponse:Response.status(property)Response.status:number
The status read-only property of the Response interface contains the HTTP status codes of the response.
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.
Returns 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.
searchString
search string
position
If position is undefined, 0 is assumed, so as to search all of the String.
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.
);if(responseconstresponse:Response|undefined){returnresponseconstresponse:Response;} // if there's no cache, then just error out // as there is nothing we can do to respond to this requestthrowerr(localvar)err:unknown;}}event(parameter)event:FetchEvent.respondWith(method)FetchEvent.respondWith(r:Response|PromiseLike<Response>):void
The respondWith() method of FetchEvent prevents the browser's default fetch handling, and allows you to provide a promise for a Response yourself.
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.
build and prerendered are empty arrays during development
Manual registration
You can disable automatic registration if you need to register the service worker with your own logic. The default registration looks something like this:
import{dev(alias)constdev:booleanimportdev
Whether the dev server is running. This is not guaranteed to correspond to NODE_ENV or MODE.
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
The 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.
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:
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
The 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.
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.
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.
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.