/how-to-make-a-sveltekit-app-work-offline/cover.jpg

How to make a SvelteKit App work offline?

Motivation

Sometimes we want our applications to be usable without the need for an internet connection. Maybe save some blog posts or create a whole fletched PWA. Maybe you have a big library you want to be cached for users, so they don’t have to download everything all over again.

This is where a service worker will help you greatly. It might be a little tricky to get it to work though. Especially on apple devices since they of course need “special” treatment.

If you want to quickly see an example implementation: https://github.com/Myrmod/SvelteKit-offline

How to add a Service Worker to SvelteKit

Adding a service worker to SvelteKit is quite easy. As mention in the documentation they made some preparation for this as well: https://kit.svelte.dev/docs/service-workers

In SvelteKit, if you have a src/service-worker.js file (or src/service-worker.ts, or src/service-worker/index.js, etc) it will be built with Vite and automatically registered. So that’s of course what we go with, we add src/service-worker/index.ts.

js
// service-worker/index.ts
import fetchEvent from './fetchEvent';
import installEvent from './installEvent';
// has to be var, because we need function scope
declare var self: ServiceWorkerGlobalScope;
/**
* Takes care of the installation of the service worker, as well as the creation of the cache.
*/
self.addEventListener('install', installEvent);
/**
* Intercepts requests made by the page so we can decide what to do with each one.
*/
self.addEventListener('fetch', fetchEvent);

Here we have two events. The the install event will be called once at the bgeinning of the live cycle of our service worker. This happens directly after registering. The fetch event is some kind of reverse proxy. It’s used to handle out going requests and to serve cached data instead of newly loaded one. For the install event we have this

js
// service-worker/installEvent.ts
import { CACHE_NAME } from './constants';
import { build } from '$service-worker';
export default (event: ExtendableEvent): void => {
console.log('installing service worker');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
// Open a cache and cache our files
cache.addAll(build);
return true;
}),
);
};

This will be called once you open you page and the service worker can be registered automatically. The cache name will be used in the fetchEvent.ts as well so we place it in another variable.

In SvelteKit we are able to import some predefined stuff using $service-worker. One of those things is the build. That’s essentially a list of thing we’re able to cache. Here we don’t think about what to cache, we simply cache everything. It is wise to take a look into the value of build though. Just to understand what is going on. The most complicated thing is our fetchEvent.ts. As mentioned it exists to intercept our requests and serve us cached data.

js
// service-worker/fetchEvent.ts
import { CACHE_NAME } from './constants';
export default (event: FetchEvent): void => {
event.respondWith(
caches.match(event.request).then((cacheResponse) => {
if (cacheResponse) {
console.info(`fetching from cache: ${event.request.url}`);
return cacheResponse;
}
console.info(`trying to fetch from server: ${event.request.url}`);
return fetch(event.request)
.then(async (fetchResponse): Promise<Response | undefined> => {
if (
event.request.url.indexOf('http') !== -1
) {
const cache = await caches.open(CACHE_NAME);
try {
// filter what to add to the cache
if (
fetchResponse.status !== 206
) {
cache.put(event.request.url, fetchResponse.clone());
}
} catch (error) {
console.error(error);
}
return fetchResponse;
}
// eslint-disable-next-line consistent-return
return undefined;
})
.catch(((error) => {
console.error(`"${error}: ${event.request.url}`);
return error;
}));
}),
);
};

We will have things that we want to cache but might not know of on build time so the installEvent wont cache those files. We have to cache these files or requests after requesting them previously.

In this event handler we intercept every request and if we have a cached answer we simply return the cached value. If we don’t have a cached value we check if the requested value is an internal one (only external requests will have http in their URL). Then we can get ready to cache whatever we will be requesting. Lastly we check if we cache the response. For example a response of a partially loaded resource, like a video, will not be cached. After eventually caching the request we will simply return the response the way it came.

Adding PWA capability

To make our application installable and behave like a progressive web application. We will need a few extra things. In our templates head:

html
<meta name="theme-color" content="#000">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="shortcut icon" type="image/svg" href="/svelte.svg">
<link rel="apple-touch-icon" href="/svelte.svg">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="My Apps">
<meta name="application-name" content="My Apps">
<meta name="mobile-web-app-capable" content="yes">

Most of them are for our problem child Apple They’re quite self explaining though. The Webmanifest is a special file, that tells our browser some extra information.

json
// static/manifest.webmanifest
{
"short_name": "App",
"name": "my awesome App",
"start_url": "index.html",
"display": "fullscreen",
"theme_color": "#000",
"background_color": "#fff",
"icons": [
{
"src": "/logo.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any"
},
{
"src": "/logo.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
]
}

We see how our Application will be called once it is installed, how it can be displayed, some themes and icons to go with. There are tools to help you with the creation like https://app-manifest.firebaseapp.com/. This file can be simply placed in your static directory.

Wrapping up

With these files in place you can test your application. So you can run npm run build first and afterwards npm run preview. Now you should see a small log in your console, which says that the service worker has been installed.

To test it’s offline capability you either disable your network or you can run a Google Lighthouse test. This tells you what you might be missing, if it’s not working offline or is not installable.

As you have seen it’s not that difficult to create a simple service worker for your application. It’s a great thing in your toolbox if you want to save some mobile data of your recurring customers.