Integrate Workbox Service Worker into Astro Project
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:
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 Pre-caching:
Once the service worker is done pre-caching all the assets it will be activated and running.
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)
.
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: