My blog is now a Progressive Web Application

github twitter github

Lately, I have had some time to play with the Lighthouse tool when auditing a couple of rich websites. By rich, I mean a lot of animations, videos, fonts, images, and other assets. Most of the content was static or changed once a week or even less often. There is a lot that could be done to improve the performance of such a website, but I would like to focus on one aspect here - PWA (Progressive Web App), Service Workers, and the Workbox set of tools for a performance boost.

I am aware that probably not all such websites require 'installable' and fully functional PWAs. But it is still worth thinking about proper configuration, mainly because of the overall performance boost.

I also implemented it for this blog website. You can always check the source code using developer tools or even directly displaying JavaScript files in the browser. So this will also be a guide with a real-life example.

Let's start with a quick intro to the technology we will use here.

What's so special about PWA?

There are many articles about PWAs on the Internet, but we can put together a couple of important points here as TL;DR. Why it is worth having a proper PWA configuration with Service Worker and Workbox:

  1. Caching strategies.
  2. Decrease in loading times.
  3. Website is ready to be used as something very close to a mobile app (for example push notifications or add to home screen).
  4. Possibility to work offline.

How to properly configure it?

  1. You would need to register your Service Worker.
  2. You need to prepare Web App Manifest.
  3. You would need to prepare icons.

I don't want to focus here specifically on the PWA configuration. There is a lot about it on the Internet. I want to focus on Service Worker and Workbox. I want to show an example of how to prepare a basic setup. But of course, there will also be an example of the manifest file configuration.

What is a Service Worker?

Again there is a lot about it on the Internet. But let's recap quickly in very simple words. The Service Worker is built into the browser and controlled by custom JavaScript code. What it does is a couple of things. The most important are intercepting the outgoing HTTP calls, managing the web app when offline, handling the background tasks and push notifications.

It has a non-blocking nature because it lives on a different JavaScript thread. It doesn't have access to the DOM.

In our case, the most critical functionality is managing the assets and routing, serving cached resources. It isn't straightforward without proper tooling because we would need to use Cache API to configure everything from the ground. It is why we will use the Workbox.

What is the Workbox?

Workbox is a set of tools designed to help with Service Worker and caching configuration. The library is split into many different modules. Each of them is responsible for some small functionality.

To use the Workbox with a static website, which this blog is. We would need to use the CDN version of the Workbox. Otherwise, we would need to have a bundler. And because this blog is built using a static site generator which by design doesn't use any bundler, I will use the CDN version of the library.

What you would need to do in such a case is to load it in the Service Worker file, in this case, sw.js (located in the root of the website).

// /sw.js
self.importScripts(
  'https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js'
);

console.log('Hello from sw.js');
console.log('Workbox: ', self.workbox);

That's it. We now have the workbox namespace in the global self object. You would be able to use Workbox modules by referencing them like: workbox.precaching.*, workbox.routing.*. We will go back to it later in the article.

The Service Worker registration

The next step would be to register the Service Worker. You can do this in any place on the website. I prepared a separate file for it sw-init.js

// /assets/js/sw-init.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}

If you are interested in how to do it using the Harold static site generator, check out some documentation on the statics directory here.

Remember that it will only work with SSL encryption. You can test it without the SSL only with localhost.

Basic Workbox configuration

Here on julian.io, I wanted to cache a couple of resources. Mainly fonts, images, styles, and navigation. I've pulled the basic configuration from here. You can learn more by reading the comments in the source code of the Service Worker here. The configuration has excellent documentation under linked sources, so that I won't copy it here, but what is worth mentioning are the differences between the caching strategies. Workbox allows for a couple of them:

  1. Stale-While-Revalidate - respond to a request as quickly as possible using the cache. It will also try to update the cache.
  2. Cache first - respond using cache. Won't make a network request if the data is in the cache.
  3. Network first - it will request the network first, then cache the results. If the network isn't reachable, it will use the cache.
  4. Network only - it won't use the cache at all.
  5. Cache only - it won't use the network requests at all.

An example of a configuration for image caching:

// Cache images with a Cache First strategy
registerRoute(
  // Check to see if the request's destination is style for an image
  ({ request }) => request.destination === 'image',
  // Use a Cache First caching strategy
  new CacheFirst({
    // Put all cached files in a cache named 'images'
    cacheName: 'images',
    plugins: [
      // Ensure that only requests that result in a 200 status are cached
      new CacheableResponsePlugin({
        statuses: [200],
      }),
      // Don't cache more than 50 items, and expire them after 30 days
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days
      }),
    ],
  })
);

You will find much more explanation with the configuration examples here.

Web App Manifest and icons

Last but not least is the Web App Manifest. Without it, we won't have the proper PWA. The Lighthouse's report will show that the PWA isn't configured even though the Service Worker and Workbox are already doing their work.

To simplify our lives, we can use one of many manifest generators. For example this one. It will generate all the icons too. In the end, we will get a zip package with the manifest and icons. We can change the paths and move the icons. The manifest should stay at the root of the website.

It looks like that:

{
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "display": "browser",
  "scope": "/",
  "start_url": "/",
  "name": "julian.io",
  "short_name": "julian.io",
  "description": "Web developer - Sometimes blockchain and crypto",
  "icons": [
    {
      "src": "/assets/images/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-512x512.png",
      "sizes": "1024x1024",
      "type": "image/png",
      "purpose": "maskable"
    },
  ]
}

What is important you would also need to use a couple of new meta tags:

<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/assets/images/icon-192x192.png">
<meta name="theme-color" content="#000000"/>

Frontend frameworks and PWA

There is a lot of tooling around the most known frontend frameworks. Let's take a look at the most popular ones. Nuxt in the Vue ecosystem and Next in the React ecosystem.

Nuxt

For Nuxt, we have a beneficial tool. It is @nuxtjs/pwa.

It simplifies the whole configuration of a Service Worker, Web App Manifest, and it also incorporates the Workbox tooling, which is excellent. The only work which has to be done is to add the @nuxtjs/pwa as buildModule and prepare proper configuration. You'll find a lot of information in the documentation.

Let's see how we can configure a basic setup:

// nuxt.config.js
// (...)
{
  buildModules: [
    '@nuxtjs/pwa',
  ]
}
// (...)

Then based on the docs, we can prepare a configuration like:

// nuxt.config.js
{
  pwa: {
    icon: {...},
    mata: {...},
    manifest: {...},
    workbox: {...}
  }
}

You'll find all the options starting from here.

Next

When it comes to Next, there are very similar tools. We have next-pwa package, which I think is a little bit more complicated when configuring it, but it still simplifies a lot of stuff. It also uses the Workbox tooling under the hood, which is perfect.

What you need to do is to add the configuration similar to this one:

// next.config.js
const withPWA = require('next-pwa')

module.exports = withPWA({
  pwa: {
    dest: 'public',
    // disable: process.env.NODE_ENV === 'development',
    // register: true,
    // scope: '/app',
    // sw: 'service-worker.js',
    //...
  }
})

You'll find all the information about the options here

Summary

I had some fun with it on occasion, so I thought that it would be good to write it down, to be able to back to it when needed in the future.

Julian.io blog also implements that, so everyone can always preview the source code of the Service Worker and how the Workbox is configured.

Of course, you will find many great resources on the Internet. Search for PWA, Workbox, Service Worker keywords.