workbox-window
workbox-window
.The workbox-window
package is a set of modules that are intended to run in the window
context, which is to say, inside of your web pages. They're a complement to the other workbox packages that run in the service worker.
The key features/goals of workbox-window
are:
- To simplify the process of service worker registration and updates by helping developers identify the most critical moments in the service worker lifecycle, and making it easier to respond to those moments.
- To help prevent developers from making the most common mistakes.
- To enable easier communication between code running in the service worker and code running in the window.
Importing and using workbox-window
The primary entry point for workbox-window
package is the Workbox
class, and you can import it in your code either from our CDN or using any of the popular JavaScript bundling tools.
Using our CDN
The easiest way to import the Workbox
class on your site is from our CDN:
<script type="module">
import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-window.prod.mjs';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
}
</script>
Note that this example uses <script type="module">
and the import
statement to load the Workbox
class. While you might think that you need to transpile this code to get it working in older browsers, that's actually not necessary.
All major browsers that support service worker also support native JavaScript modules, so it's perfectly fine to serve this code to any browsers (older browsers will just ignore it).
Loading Workbox with JavaScript bundlers
While absolutely no tooling is required to use workbox-window
, if your development infrastructure already includes a bundler like webpack or Rollup that works with npm dependencies, it's possible to use them to load workbox-window
.
The first step is to install workbox-window
as a dependency of your application:
npm install workbox-window
Then, in one of your application's JavaScript files, import
workbox by referencing the workbox-window
package name:
import {Workbox} from 'workbox-window';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
}
If your bundler supports code splitting via dynamic import statements, you can also conditionally load workbox-window
, which should help reduce the size of your page's main bundle.
Even though workbox-window
is quite small, there's no reason it needs to be loaded with your site's core application logic, as service workers, by their very nature, are a progressive enhancement.
if ('serviceWorker' in navigator) {
const {Workbox} = await import('workbox-window');
const wb = new Workbox('/sw.js');
wb.register();
}
Advanced bundling concepts
Unlike the Workbox packages that run in the service worker, the build files referenced by workbox-window
's main
and module
fields in package.json
are transpiled to ES5. This makes them compatible with today's build tools—some of which do not allow developers to transpile anything of their node_module
dependencies.
If your build system does allow you to transpile your dependencies (or if you don't need to transpile any of your code), then it's better to import a specific source file rather than the package itself.
Here are the various ways you can import Workbox
, along with an explanation of what each will return:
// Imports a UMD version with ES5 syntax
// (pkg.main: "build/workbox-window.prod.umd.js")
const {Workbox} = require('workbox-window');
// Imports the module version with ES5 syntax
// (pkg.module: "build/workbox-window.prod.es5.mjs")
import {Workbox} from 'workbox-window';
// Imports the module source file with ES2015+ syntax
import {Workbox} from 'workbox-window/Workbox.mjs';
If you're importing the source file directly, you'll also need to configure your build process to minify the file, and remove development-only code when you deploy it to production. See the guide Using Bundlers (webpack/Rollup) with Workbox for more details.
Examples
Once you've imported the Workbox
class, you can use it to register and interact with your service worker. Here are some examples of ways you might use Workbox
in your application:
Register a service worker and notify the user the very first time that service worker is active
Many web applications user service worker to precache assets so their app works offline on subsequent page loads. In some cases it could make sense to inform the user that the app is now available offline.
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', event => {
// `event.isUpdate` will be true if another version of the service
// worker was controlling the page when this version was registered.
if (!event.isUpdate) {
console.log('Service worker activated for the first time!');
// If your service worker is configured to precache assets, those
// assets should all be available now.
}
});
// Register the service worker after event listeners have been added.
wb.register();
Notify the user if a service worker has installed but is stuck waiting to activate
When a page controlled by an existing service worker registers a new service worker, by default that service worker will not activate until all clients controlled by the initial service worker have fully unloaded.
This is a common source of confusion for developers, especially in cases where reloading the current page doesn't cause the new service worker to activate.
To help minimize confusion and make it clear when this situation is happening, the Workbox
class provides a waiting
event that you can listen for:
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', event => {
console.log(
`A new service worker has installed, but it can't activate` +
`until all tabs running the current version have fully unloaded.`
);
});
// Register the service worker after event listeners have been added.
wb.register();
workbox-broadcast-update
package
Notify the user of cache updates from the The workbox-broadcast-update
package is a great way to be able to serve content from the cache (for fast delivery) while also being able to inform the user of updates to that content (using the stale-while-revalidate strategy).
To receive those updates from the window, you can listen to message
events of type CACHE_UPDATED
:
const wb = new Workbox('/sw.js');
wb.addEventListener('message', event => {
if (event.data.type === 'CACHE_UPDATED') {
const {updatedURL} = event.data.payload;
console.log(`A newer version of ${updatedURL} is available!`);
}
});
// Register the service worker after event listeners have been added.
wb.register();
Send the service worker a list of URLs to cache
For some applications, it's possible to know all the assets that need to be precached at build time, but some applications serve completely different pages, based on what URL the user lands on first.
For apps in the latter category, it might make sense to only cache the assets the user needed for the particular page they visited. When using the workbox-routing
package, you can send your router a list of URLs to cache, and it will cache those URLs according to the rules defined on the router itself.
This example sends a list of URLs loaded by the page to the router any time a new service worker is activated. Note, it's fine to send all URLs because only the URLs that match a defined route in the service worker will be cached:
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', event => {
// Get the current page URL + all resources the page loaded.
const urlsToCache = [
location.href,
...performance.getEntriesByType('resource').map(r => r.name),
];
// Send that list of URLs to your router in the service worker.
wb.messageSW({
type: 'CACHE_URLS',
payload: {urlsToCache},
});
});
// Register the service worker after event listeners have been added.
wb.register();
The above technique works for any route defined via the registerRoute()
method on the default router. If you're creating your own Router
instance, you'll need to call its addCacheListener()
method manually.
Important service worker lifecycle moments
The service worker lifecycle is complex and can be a challenge to fully understand. Part of the reason it's so complex is it must handle all the edge cases for all possible usages of service worker (e.g. registering more than one service worker, registering different service workers in different frames, registering service workers with different names, etc.).
But most developers implementing service worker should not need to worry about all these edge cases because their usage is quite simple. Most developer register just one service worker per page load, and they don't change the name of the service worker file they deploy to their server.
The Workbox
class embraces this simpler view for the service worker lifecycle by breaking all service worker registrations into two categories: the instance's own, registered service worker and an external service worker:
- Registered service worker: a service worker that started installing as a result of the
Workbox
instance callingregister()
or the already-active service worker if callingregister()
did not trigger anupdatefound
event on the registration. - External service worker: a service worker that started installing independently of the
Workbox
instance callingregister()
. This typically happens when a user has a new version of your site open in another tab. When an event originates from an external service worker, the event'sisExternal
property will be set totrue
.
With these two types of service workers in mind, here is a breakdown of all the important service worker lifecycle moments, along with developer recommendations for how to handle them:
The very first time a service worker is installed
You'll probably want to treat the very first time a service worker install differently from how you treat all future updates.
In workbox-window
, you can differentiate between the version first installation and future updates by checking the isUpdate
property on any of the following events. For the very first installation, isUpdate
will be false
.
const wb = new Workbox('/sw.js');
wb.addEventListener('installed', event => {
if (!event.isUpdate) {
// First-installed code goes here...
}
});
wb.register();
When an updated version of the service worker is found
When a new service worker starts installing but an existing version is currently controlling the page, the isUpdate
property of all the following events will be true
.
How you react in this situation is typically different from the very first installation because you have to manage when and how the user gets this update.
When an unexpected version of the service worker is found
Sometimes users will keep your site open in a background tab for a very long time. They might even open a new tab and navigate to your site without realizing they already have your site open in a background tab. In such cases it's possible to have two versions of your site running at the same time, and that can present some interesting problems for you as the developer.
Consider a scenario where you have tab A running v1 of your site and tab B running v2. When tab B loads, it'll be controlled by the version of your service worker that shipped with v1, but the page returned by the server (if using a network-first caching strategy for your navigation requests) will contain all your v2 assets.
This is generally not a problem for tab B though, since when you wrote your v2 code, you were aware of how your v1 code worked. However, it could be a problem for tab A, since your v1 code could not have possibly predicted what changes your v2 code might introduce.
To help handle these situations, workbox-window
also dispatches lifecycle events when it detects an update from an "external" service worker, where external just means any version that is not the version the current Workbox
instance registered.
As of Workbox v6 and later, these events are equivalent to the events documented above, with the addition of an isExternal: true
property set on each event object. If your web application needs to implement specific logic to handle an "external" service worker, you can check for that property in your event handlers.
Avoiding common mistakes
One of the most helpful features Workbox provides is it's developer logging. And this is especially true for workbox-window
.
We know developing with service worker can often be confusing, and when things happen contrary to what you'd expect, it can be hard to know why.
For example, when you make a change to your service worker and reload the page, you might not see that change in your browser. The most likely reason for this, is your service worker is still waiting to activate.
But when registering a service worker with the Workbox
class, you'll be informed of all lifecycle state changes in the developer console, which should help with debugging why things aren't as you'd expect.
In addition, a common mistake developers make when first using service worker is to register a service worker in the wrong scope.
To help prevent this from happening, the Workbox
class will warn you if the page registering the service worker is not in that service worker's scope. It'll also warning you in cases where your service worker is active but not yet controlling the page:
Window to service worker communication
Most advanced service worker usage involves a lots of messaging between the service worker and the window. The Workbox
class helps with this as well by providing a messageSW()
method, which will postMessage()
the instance's registered service worker and await a response.
While you can send data to the service worker in any format, the format shared by all Workbox packages is an object with three properties (the latter two being optional):
Messages sent via the messageSW()
method use MessageChannel
so the receiver can respond to them. To respond to a message you can call event.ports[0].postMessage(response)
in your message event listener. The messageSW()
method returns a promise that will resolve to whatever response
you reply with.
Here's an example of sending messages from the window to the service worker and getting a response back. The first code block is the message listener in the service worker, and the second block uses the Workbox
class to send the message and await the response:
Code in sw.js:
const SW_VERSION = '1.0.0';
addEventListener('message', event => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
Code in main.js (running in the window):
const wb = new Workbox('/sw.js');
wb.register();
const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);
Managing version incompatibilities
The example above show how you might implement checking the service worker version from the window. This example is used because when you're sending messages back and forth between the window and the service worker, it's critical to be aware that your service worker might not be running the same version of your site that your page code is running, and the solution for dealing with this problem is different depending on whether your serving your pages network-first or cache-first.
Network first
When serving your pages network first, your users will always be getting the latest version of your HTML from your server. However, the first time a user revisits your site (after you've deployed an update) the HTML they get will be for the latest version, but the service worker running in their browser will be a version installed previously (possibly many versions old).
It's important to understand this possibility because if the JavaScript loaded by the current version of your page sends a message to an older version of your service worker, that version may not know how to respond (or it may respond with an incompatible format).
As a result, it's a good idea to always version your service worker and check for compatible versions before doing any critical work.
For example, in the code above, if the service worker version returned by that messageSW()
call is older than the expected version, it would be wise to wait until an update is found (which should happen when you call register()
). At that point you can either notify the user or an update, or you can manually skip the waiting phase to activate the new service worker right away.
Cache first
As opposed to when you serve pages network-first, when serving your pages cache- first, you know your page is initially always going to be the same version as your service worker (because that's what served it). And as a result, it's safe to use messageSW()
right away.
However, if an updated version of your service worker is found and activates when your page calls register()
(i.e. you intentionally skip the waiting phase), it may no longer be safe to send messages to it.
One strategy for managing this possibility is to use a versioning scheme that allows you to differentiate between breaking updates and non-breaking updates, and in the case of a breaking update you'd know it's not safe to message the service worker. Instead you'd want to warn the user that they're running an old version of the page, and suggest they reload to get the update.
Skip waiting helper
A common use convention for window to service worker messaging is send a {type: 'SKIP_WAITING'}
message to instruct a service worker that's installed to skip the waiting phase and activate.
Starting with Workbox v6, the messageSkipWaiting()
method can be used to send a {type: 'SKIP_WAITING'}
message to the waiting service worker associated with the current service worker registration. It will silently do nothing if there isn't a waiting service worker.
For further guidance on using messageSkipWaiting()
, see the "Handling service worker updates with immediacy" recipe.