Andrew Evans Husband, engineer, FOSS contributor, and manager at CapTech. Follow me at rhythmandbinary.com and andrewevans.dev.

Building a PWA with Svelte

5 min read 1664

PWA Svelte Sapper SvelteKit

Similar to native applications, progressive web apps (PWAs) are a popular solution to running web applications on mobile devices. With PWAs, users can experience web applications with the same ease and familiarity as mobile apps; similarly, companies are able to host apps directly on the internet instead of different mobile app stores.

In this tutorial, we’ll create our own PWA with Svelte, a modern framework that is based on imperative vs. declarative applications. With traditional frameworks like React, you must individually build out all of the pieces of your application; however, Svelte can pre-render your application’s build with just a small amount of code.

First, we’ll explore how PWAs work by building our own in Svelte. Then, we’ll look at some features of Sapper and SvelteKit that you can use to bring PWA features to your apps. We’ll be referencing a sample project that can be accessed at my repo on GitHub. Let’s get started!

PWA features

Before building our own, let’s take a closer look at how PWAs work. The following two features work in conjunction with progressive enhancement to create an experience similar to native applications.

Service workers act as intermediaries or proxies for web applications, enabling the use of caching resources and handling poor internet connections gracefully.

For example, when internet connection is lost, we may use a service worker to display a message to a user so that the app does not crash suddenly. Similarly, a service worker may cause our app to save local activity and resync after regaining internet connection.

Web manifests enable users to download or install apps on specific platforms. The app that the user sees in the browser can be run offline or in a state similar to a native implementation. Although manifests are still considered experimental, they are heavily supported by modern browsers.

Build a PWA with Svelte

Now that we understand the features that define PWAs, let’s build our own using Svelte. The example found in this section is based on an example from GitHub.

First, head to your terminal and create a new Svelte project by running the code below:

npx degit sveltejs/template svelte-pwa

Next, we’ll install our JavaScript dependencies. Navigate into the directory that you just created and run the code below:

We made a custom demo for .
No really. Click here to check it out.

cd svelte-pwa
npm install

When we used degit to create a new project, Svelte scaffolded a basic app with the following structure:

Scaffolded Svelte App Structure

We’ll primarily be working with the files in the src directory. The App.svelte file is our project’s entry point and contains the following code:

<script>
    export let name;
</script>
<main>
    <h1>Hello {name}!</h1>
    <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>
<style>
    main {
        text-align: center;
        padding: 1em;
        max-width: 240px;
        margin: 0 auto;
    }
    h1 {
        color: #ff3e00;
        text-transform: uppercase;
        font-size: 4em;
        font-weight: 100;
    }
    @media (min-width: 640px) {
        main {
            max-width: none;
        }
    }
</style>

.svelte files have separate sections for styles, the view html, and any JavaScript code <scripts>.

Add the following code to your main.js file to bootstrap your app and tell the bundler to build the project with your .svelte App file:

import App from './App.svelte';
const app = new App({
    target: document.body,
    props: {
        name: 'world'
    }
});
export default app;

To create a PWA, first, we’ll create a service worker inside of the service-worker.js file in the public directory:

"use strict";
// Cache Name
const CACHE_NAME = "static-cache-v1";
// Cache Files
const FILES_TO_CACHE = ["/offline.html"];
// install
self.addEventListener("install", (evt) => {
  console.log("[ServiceWorker] Install");
  evt.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("[ServiceWorker] Pre-caching offline page");
      return cache.addAll(FILES_TO_CACHE);
    })
  );
  self.skipWaiting();
});
// Active PWA Cache and clear out anything older
self.addEventListener("activate", (evt) => {
  console.log("[ServiceWorker] Activate");
  evt.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(
        keyList.map((key) => {
          if (key !== CACHE_NAME) {
            console.log("[ServiceWorker] Removing old cache", key);
            return caches.delete(key);
          }
        })
      );
    })
  );
  self.clients.claim();
});
// listen for fetch events in page navigation and return anything that has been cached
self.addEventListener("fetch", (evt) => {
  console.log("[ServiceWorker] Fetch", evt.request.url);
  // when not a navigation event return
  if (evt.request.mode !== "navigate") {
    return;
  }
  evt.respondWith(
    fetch(evt.request).catch(() => {
      return caches.open(CACHE_NAME).then((cache) => {
        return cache.match("offline.html");
      });
    })
  );
});

In the code block above, we register events handled by the service worker, including install and fetch events. To handle offline usage of the project, we’ll cache files used in navigation and record what we cache. Using PWAs, you can cache only what is needed, improving your UX.

Next, we’ll create an offline.html file, which will be cached to navigation. Add the following code in the public directory:

Svelte Cache Public Directory

Now, let’s create a manifest.json file in the public directory:

Svelte Manifest JSON

You can use the icons entry to set install icons for different devices, however, I left it blank for simplicity.

Now, when Svelte builds your app, it will read these files and create a running service worker that caches a navigation resource whenever offline activity is detected. Running npm run dev will start your app with the service worker. If you open DevTools in Chrome, you’ll see the service worker starting:

Svelte App Run Service Worker

To see the service worker in action, you can also go into the network tab and move your session offline:

Svelte Service Worker Offline

Svelte Final Service Worker

Build a PWA in SvelteKit

Now, let’s create a PWA using SvelteKit. For this example, we’ll create a “Hello, World!” application. Run the code below in your terminal:

npm init [email protected] sveltekit-pwa

The CLI will ask you if you’d like to create a demo app or a skeleton project. Select demo app:

Svelte Sapper CLI Demo App

The CLI will also ask you if you’d like to use TypeScript, Prettier, or ESLint. Add the configuration below:

Svelte Sapper TypeScript Prettier Eslint Configuration

Now, head into your SvelteKit project directory and install the required dependencies with the following code:

cd sveltekit-pwa
npm install

To run your project, add the code below:

npm run dev -- --open

New SvelteKit App Homepage

Now that our project is created, we can make it a PWA using the same steps we followed to create a basic Svelte PWA. Create a manifest.json file in the static directory:

SvelteKit PWA Manifest Json

Next, modify the app.html file in the src directory to include a reference to the manifest.json file:

Reference Manifest Json File

Lastly, create a service-worker.js file in the src directory:

import { build, files, timestamp } from '$service-worker';
const worker = (self as unknown) as any;
const FILES = `cache${timestamp}`;
const to_cache = build.concat(files);
const staticAssets = new Set(to_cache);
// listen for the install events
worker.addEventListener('install', (event) => {
    event.waitUntil(
        caches
            .open(FILES)
            .then((cache) => cache.addAll(to_cache))
            .then(() => {
                worker.skipWaiting();
            })
    );
});
// listen for the activate events
worker.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then(async (keys) => {
            // delete old caches
            for (const key of keys) {
                if (key !== FILES) await caches.delete(key);
            }
            worker.clients.claim();
        })
    );
});
// attempt to process HTTP requests and rely on the cache if offline
async function fetchAndCache(request: Request) {
    const cache = await caches.open(`offline${timestamp}`);
    try {
        const response = await fetch(request);
        cache.put(request, response.clone());
        return response;
    } catch (err) {
        const response = await cache.match(request);
        if (response) return response;
        throw err;
    }
}
// listen for the fetch events
worker.addEventListener('fetch', (event) => {
    if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
    const url = new URL(event.request.url);
    // only cache files that are local to your application
    const isHttp = url.protocol.startsWith('http');
    const isDevServerRequest =
        url.hostname === self.location.hostname && url.port !== self.location.port;
    const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname);
    const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset;
    if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
        event.respondWith(
            (async () => {
                // always serve static files and bundler-generated assets from cache.
                // if your application has other URLs with data that will never change,
                // set this variable to true for them and they will only be fetched once.
                const cachedAsset = isStaticAsset && (await caches.match(event.request));
                return cachedAsset || fetchAndCache(event.request);
            })()
        );
    }
});

With our SvelteKit PWA finished, we can connect it to the internet and see it running:

SvelteKit PWA Internet Connection

Without internet connection, our app will look like the image below:

SvelteKit PWA Without Internet

Despite the obvious HTTP errors, the app is able to gracefully handle being disconnected from the internet.

Build a PWA in Sapper

As stated in the official Sapper docs: Sapper’s succesor, SvelteKit, is currently available for use. All development efforts moving forward will be focused on SvelteKit. The Sapper docs include a helpful migration guide. If you prefer to build you application with Sapper despite the lack of support, read ahead:

Sapper allows you to develop your app as you would with Svelte, however, it has the added benefit of support for PWAs. Create a Sapper application by running the following code in your terminal:

npx degit "sveltejs/sapper-template#rollup" sapper-pwa

Install the required dependencies by running the code below:

cd sapper-pwa
npm install

Now, to see your Sapper application in action, add the local run command as follows:

npm run dev

Svelte Sapper PWA Template

Looking at the generated code, you’ll see that the project already includes a service worker and a manifest file:

Svelte Sapper Service Worker Manifest File

With this process, you can basically develop a Sapper application just like you would with Svelte.

Conclusion

In this article, we explored the benefits of building PWAs and examined how they are built using service workers and web manifests. Then, we explored three methods for building PWAs using Svelte, SvelteKit, and Sapper.

PWAs are likely going to continue growing in use due to the popularity of native applications. Thankfully, building a PWA is fairly straightforward when you use a modern framework like Svelte. I hope you enjoyed this tutorial!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Andrew Evans Husband, engineer, FOSS contributor, and manager at CapTech. Follow me at rhythmandbinary.com and andrewevans.dev.

Leave a Reply