Skip to main

Integrate Workbox Service Worker into Astro Project

8 min read

Integrating a Service worker to your website is a great way to progressively enhance the sites performance as well as supporting features like offline content viewing, background push etc. Service Workers even though as simple as they are can be a bit daunting at first and a bit tricky to deal with especially when it comes to things like pre-caching, pruning old caches or even handling events across the service worker and web app. Workbox is a set of well tested production ready tools/libraries that helps simplify these things.

Installing Workbox

Workbox can be setup in various ways like using workbox-cli, workbox-build node package or just using a bundler plugin. All of the methods more or less do one thing that is to generate the final sw.js service worker file which can be registered in your HTML document using navigator.serviceWorker.register method.

Here i’ll be using the workbox-cli to generate my service worker file. Let’s install it using package manager.

npm install -D workbox-cli

Create a workbox.config.cjs file in the root directory of your project. This is the config file workbox-cli will use to generate our service worker file.

Let’s add some config options to workbox.config.cjs:

const globPath = process.env.SW_DIST_PATH;

if (!globPath) {
  console.error("SW_DIST_PATH not specified. Check your npm build script");
  return;
}

module.exports = {
  globDirectory: globPath,
  globPatterns: [
    "**/*.{css,png,webp,avif,mp4,html,ico,woff2,json,js,svg,xml,txt}",
  ],
  swDest: `${globPath}/sw.js`,
};

The globPatterns option specifies all the file types we want our service worker to pre-cache when it installs and globDirectory is the path where the workbox-cli should look for to match the file type patterns, So the globDirectory is basically the build output path of our astro build command. Since we can target different adapters to build for in our Astro project I have moved out the globDirectory value into the environment variable for some extra flexibility. The swDest directory is where the service worker file will be written to.

Let’s use the above config file in workbox-cli to generate out service worker file. First we’ll need to update our build scripts in package.json file so we can specify our SW_DIST_PATH environment variable value.

In our package.json:

{
  "scripts": {
    "build": "astro build && npm run generateSW",
    "generateSW": "SW_DIST_PATH=dist/client workbox generateSW workbox.config.cjs"
  }
}

I’m using Astro Node Adapter in this example that outputs build files into dist/client directory. So when we run generateSW command the workbox cli will look for static assets in the correct directory and include the file paths in the sw.js file. Now we can include this sw.js file in our Astro project and register it.

Generated sw.js file in the build directory:

Pre-cache urls in the generated sw.js file

In your Layout.astro or common layout file we can register this service worker like this:

<head>
  <script is:inline>
    addEventListener("load", () => {
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("/sw.js")
          .then(() => {
            console.log("Service Worker Registered");
          })
          .catch((err) => {
            console.log("Some error occurred while registering service worker");
            console.log(err);
          });
      }
    });
  </script>
</head>

If we run the preview server (npm run preview) and check our Dev Tools we can see that our service worker is registered and installing.

Service Worker installing

Service Worker Pre-caching:

Service Worker Precaching

Once the service worker is done pre-caching all the assets it will be activated and running.

Service Worker Activated

Now any requests that are pre-cached will be returned by our service worker. Workbox handles all the pruning of old caches, revisions, updating sw.js etc for us.

Customising our service worker

There are mainly two ways of generating our service worker using workbox-cli, generateSW and injectManifest. Since we use generateSW command from the workbox-cli we don’t have much control over the final sw.js file. If we want to add some custom logic to our service worker along with workbox tools we can use injectManifest command. This let’s us pass in our custom logic into the final sw.js file

Let’s create a custom service worker using workbox-cli but still keep the pre-caching capabilities of workbox. That’s where workbox-modules come into play. Pre-caching routes is actually a workbox module that we get for free while using generateSW command from workbox. First let’s create a sw.js file in public directory of our astro project and add pre-caching module from workbox to it.

sw.js in public directory:

import { precacheAndRoute, cleanupOutdatedCaches } from "workbox-precaching";

cleanupOutdatedCaches();

// Precache the manifest
precacheAndRoute(self.__WB_MANIFEST);

console.log("Hello. Some custom logic here...");

Here self.__WB_MANIFEST is a special value where workbox will inject the list of URLs that are needed to be pre-cached using the globDirectory and globPatterns config. cleanupOutdatedCaches method will clean-up old and incompatible caches created previously by older versions of workbox. Let’s add this sw.js file as input in our workbox.config.cjs file:

const globPath = process.env.SW_DIST_PATH;

if (!globPath) {
  console.error("SW_DIST_PATH not specified. Check your npm build script");
  return;
}

module.exports = {
  globDirectory: globPath,
  globPatterns: [
    "**/*.{css,png,webp,avif,mp4,html,ico,woff2,json,js,svg,xml,txt}",
  ],
  swDest: `${globPath}/sw.js`,
  swSrc: "./public/sw.js", // Our custom sw.js file
};

Also let’s change the npm package scripts from generateSW to injectManifest in our workbox-cli command:

{
  "scripts": {
    "build": "astro build && npm run generateSW",
    "generateSW": "SW_DIST_PATH=dist/client workbox injectManifest workbox.config.cjs"
  }
}

Now if we build the astro project using astro build we should get an sw.js file in the build output directory with our custom service worker logic and workbox pre-caching capabilities.

Built sw.js file from dist/client:

import { precacheAndRoute } from "workbox-precaching";
// Precache the manifest
precacheAndRoute([
  {
    revision: "6f5efcdd674ddc792168251976c87ca1",
    url: "_astro/_slug_.320abdb5.css",
  },
  { revision: "307aca520857f32f274358d4e881e381", url: "uses/index.html" },
  // Some more Pre-cache urls...
]);

console.log("Hello. Some custom logic here...");

If we start the preview server (npm run preview) we can see that our service worker doesn’t register and throws out an error: Uncaught SyntaxError: Cannot use import statement outside a module (at sw.js:1:1).

Error registering the service worker

That is because when we use injectManifest workbox doesn’t bundle the dependencies so we need to bundle the dependencies and compile the final sw.js ourselves.

We can use any bundler that we like, I’ll be using esbuild for this. Install esbuild using npm install --save-exact esbuild and create a bundle-sw.mjs file and configure it to bundle our final sw.js file like so:

import * as esbuild from "esbuild";
const globPath = process.env.SW_DIST_PATH;

// bundle the sw file for browser use
await esbuild.build({
  entryPoints: [`${globPath}/sw.js`],
  outfile: `${globPath}/sw.js`,
  target: ["es2020"],
  bundle: true,
  minify: true,
  allowOverwrite: true,
  sourcemap: true,
});

Let’s now modify our npm package script to add esbuild bundle stage.

{
  "scripts": {
    "build": "astro build && npm run generateAndBundleSW",
    "generateAndBundleSW": "SW_DIST_PATH=dist/client workbox injectManifest workbox.config.cjs && node bundle-sw.mjs"
  }
}

Now if we astro build and preview the build we should have our custom service worker working.

Since we now can add custom logic to our service worker let’s add another workbox module that adds Navigation Preload capabilities to our site with the help of workbox-navigation-preload module. Navigation Preload fixes the delay in navigation requests caused by service worker boot time.

The following example is from Navigation Preload for Network-first HTML:

// public/sw.js
import * as navigationPreload from "workbox-navigation-preload";
import { NetworkFirst, StaleWhileRevalidate } from "workbox-strategies";
import { registerRoute, NavigationRoute, Route } from "workbox-routing";
import { precacheAndRoute } from "workbox-precaching";

// Precache the manifest
precacheAndRoute(self.__WB_MANIFEST);

// Enable navigation preload
navigationPreload.enable();

// Create a new navigation route that uses the Network-first, falling back to
// cache strategy for navigation requests with its own cache. This route will be
// handled by navigation preload. The NetworkOnly strategy will work as well.
const navigationRoute = new NavigationRoute(
  new NetworkFirst({
    cacheName: "navigations",
  })
);

// Register the navigation route
registerRoute(navigationRoute);

// Create a route for image, script, or style requests that use a
// stale-while-revalidate strategy. This route will be unaffected
// by navigation preload.
const staticAssetsRoute = new Route(
  ({ request }) => {
    return ["image", "script", "style"].includes(request.destination);
  },
  new StaleWhileRevalidate({
    cacheName: "static-assets",
  })
);

// Register the route handling static assets
registerRoute(staticAssetsRoute);

Now once this service worker is activated the navigation requests should start in parallel to service worker boot rather than waiting for service worker to boot up first and thus fixing the slight delay.

There are many more workbox modules that can be integrated into our service worker. You can check out Workbox Modules for a whole list of amazing workbox modules.

Alternate Way to install Workbox SW / PWA in Vite

Instead of using generateSW workbox command we can also use a custom library called PWA Vite Plugin that handles most of the things regarding PWA / installing Service Workers for us, assuming your project uses Vite for build step. I highly recommend this if you just want to add a SW and forget about it since this plugin requires almost zero-config to setup. It comes with great integration for Astro as well.

Further Reading / References: