Turning any website into a PWA with (mostly) vanilla JS

Progressive web apps (PWAs) are installable, responsive web applications that work on any device (within reason) and deliver a reliable experience on unreliable networks, and even work offline (once they’ve loaded, of course).

The progressive in the name suggests that they provide an acceptable experience on as many browsers as possible1, while offering an advanced experience on modern browsers, enabling new features as they become available.

PWA availability in major web frameworks

Gatsby and Sapper offer it out of the box. It’s opt-in both in React and Angular, and Vue offers a core plugin. There’s a PWA plugin for 11ty too. So — we’re fairly well covered!

But what if we need something vanilla, or something more specific?

PWA from scratch

1. Make it installable

In practice, this is when the user can “add to home screen” on their mobile device. To achieve this, we need just a few things in index.html:

  <head>
    <title etc…>
+   <meta name="theme-color" content="#f00">
+   <link rel="shortcut icon" href="/favicon.ico">
+   <link rel="apple-touch-icon" href="/path/to/icon.png">
+   <link rel="manifest" href="/path/to/your.webmanifest">
  </head>

Now, we need to create those icons and the web manifest we refer to.

your.webmanifest (you can call this anything, with a .webmanifest extension)

{
  "name": "Your site – a cool web app",
  "short_name": "Your site",
  "description": "Here goes your awesome description",
  "icons": [
    {
      "src": "icon32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "icon512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "index.html",
  "display": "fullscreen",
  "theme_color": "#f00",
  "background_color": "#f00"
}

If you run a Lighthouse audit now, the PWA section will already show some passed checks, e.g. web app manifest meets requirements, sets a theme colour, provides a valid apple-touch-icon.

It will, however, complain that your app “does not register a service worker.” Let’s get to that!

2. Register the service worker2

This can be done manually, but today we’ll use workbox-build.

$ yarn add --dev workbox-build

In your build script, after building the app:

const { generateSW } = require('workbox-build')

generateSW({
  globDirectory: 'dist', // or your build directory
  swDest: 'dist/sw.js', // or wherever you want to put your service worker
})

The last step is to register the service worker, and we’re good to go, with a green Lighthouse report.

<script type="text/javascript">
  if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/path/to/service-worker.js');
  }
</script>

A few notes

If you have a very large number of pages linked from your front page (e.g. a blog), it may be a good idea to limit the number of pre-fetched pages or use pagination to avoid downloading old entries unnecessarily.

There are several service worker plugins for common build tools (e.g. Webpack, Rollup and Parcel) on npm — usually, it’s a good idea to use one of those for seamless integration.

Finally, if I’ve made a mistake, please let me know!


Footnotes


  1. See Advantages of web applications and Progressive Enhancement, both on MDN.

  2. By the way, service workers can do much more than make an app available offline.