In some scenarios the service worker might need to proactively communicate with any of the active tabs it controls to inform of a certain event. Examples include:
- Informing the page when a new version of the service worker has been installed, so that the page can show an "Update to refresh" button to the user to access the new functionality immediately.
- Letting the user know about a change on cached data that took place on the service worker side, by showing an indication, like: "The app is now ready to work offline", or "New version of the content available".
We'll call these types of use cases where the service worker doesn't need to receive a message from the page to start a communication "broadcast updates". In this guide we'll review different ways of implementing this type of communication between pages and service workers, by using standard browser APIs and the Workbox library.
Production cases
Tinder
Tinder PWA uses workbox-window
to listen to
important service worker lifecycle moments from the page ("installed", "controlled" and
"activated"). That way when a new service worker comes into play, it shows an "Update Available"
banner, so that they can refresh the PWA and access the latest features:
Squoosh
In the Squoosh PWA, when the service worker has cached all of the necessary assets to make it work offline, it sends a message to the page to show a "Ready to work offline" toast, letting the user know about the feature:
Using Workbox
Listen to service worker lifecycle events
workbox-window
provides a straightforward interface to listen to important service worker lifecycle
events.
Under the hood, the library uses client-side APIs like
updatefound
and statechange
and provides higher level event listeners in the workbox-window
object, making it easier for the
user to consume these events.
The following page code lets you detect every time a new version of the service worker is installed, so you can communicate it to the user:
const wb = new Workbox('/sw.js');
wb.addEventListener('installed', (event) => {
if (event.isUpdate) {
// Show "Update App" banner
}
});
wb.register();
Inform the page of changes in cache data
The Workbox package
workbox-broadcast-update
provides a standard way of notifying window clients that a cached response has been updated. This is
most commonly used along with the StaleWhileRevalidate
strategy.
To broadcast updates add a broadcastUpdate.BroadcastUpdatePlugin
to your strategy options in the
service worker side:
import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate} from 'workbox-strategies';
import {BroadcastUpdatePlugin} from 'workbox-broadcast-update';
registerRoute(
({url}) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
plugins: [
new BroadcastUpdatePlugin(),
],
})
);
In your web app, you can listen for these events like so:
navigator.serviceWorker.addEventListener('message', async (event) => {
// Optional: ensure the message came from workbox-broadcast-update
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedUrl} = event.data.payload;
// Do something with cacheName and updatedUrl.
// For example, get the cached content and update
// the content on the page.
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedUrl);
const updatedText = await updatedResponse.text();
}
});
Using browser APIs
If the functionality that Workbox provides is not enough for your needs, use the following browser APIs to implement "broadcast updates":
Broadcast Channel API
The service worker creates a BroadcastChannel
object and starts sending
messages to it. Any context (e.g. page) interested in receiving these messages can instantiate a
BroadcastChannel
object and implement a message handler to receive messages.
To inform the page when a new service worker is installed, use the following code:
// Create Broadcast Channel to send messages to the page
const broadcast = new BroadcastChannel('sw-update-channel');
self.addEventListener('install', function (event) {
// Inform the page every time a new service worker is installed
broadcast.postMessage({type: 'CRITICAL_SW_UPDATE'});
});
The page listens to these events by subscribing to the sw-update-channel
:
// Create Broadcast Channel and listen to messages sent to it
const broadcast = new BroadcastChannel('sw-update-channel');
broadcast.onmessage = (event) => {
if (event.data && event.data.type === 'CRITICAL_SW_UPDATE') {
// Show "update to refresh" banner to the user.
}
};
This is a simple technique, but its limitation is browser support: at the moment of this writing, Safari doesn't support this API.
Client API
The Client API provides a straightforward
way of communicating with multiple clients from the service worker by iterating over an array of
Client
objects.
Use the following service worker code to send a message to the last focused tab:
// Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
if (clients && clients.length) {
// Respond to last focused tab
clients[0].postMessage({type: 'MSG_ID'});
}
});
The page implements a message handler to intercept these messages:
// Listen to messages
navigator.serviceWorker.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
// Process response
}
};
Client API is a great option for cases like broadcasting information to multiple active tabs. The API is supported by all major browsers, but not all of its methods are. Check browser support before using it.
Message Channel
Message Channel requires
an initial configuration step, by passing a port from the page to the service worker, to establish a
communication channel between them. The page instantiates a MessageChannel
object and passes a
port to the service worker, via the postMessage()
interface:
const messageChannel = new MessageChannel();
// Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
The page listens to messages by implementing an "onmessage" handler on that port:
// Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
The service worker receives the port and saves a reference to it:
// Initialize
let communicationPort;
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'PORT_INITIALIZATION') {
communicationPort = event.ports[0];
}
});
From that point it can send messages to the page, by calling postMessage()
in the reference to the
port:
// Communicate
communicationPort.postMessage({type: 'MSG_ID' });
MessageChannel
might be more complex to implement, due to the need of initializing ports, but it's
supported by all major browsers.
Next steps
In this guide we explored one particular case of Window to service worker communication: "broadcast updates". The examples explored include listening to important service worker lifecycle events, and communicating to the page about changes in content or cached data. You can think of more interesting use cases where the service worker proactively communicates with the page, without receiving any message previously.
For more patterns of Window and service worker communication check out:
- Imperative caching guide: Calling a service worker from the page to cache resources in advance (e.g. in prefetching scenarios).
- Two-way communication: Delegating a task to a service worker (e.g. a heavy download), and keeping the page informed on the progress.