Service Worker Push Messaging: Notifying Active Pages
Service workers typically respond to requests initiated by the page they control. But there are cases where the service worker must proactively reach out to all active tabs, such as:
- Notifying the page that a new service worker version is installed, so it can offer an "Update to refresh" action that loads new features immediately.
- Alerting the user to changes in cached data with messaging like "The app is now ready to work offline" or "New version of the content available".
These scenarios, where the service worker initiates communication without a prior message from the page, are called "broadcast updates". There are several ways to implement them, ranging from the Workbox library to direct browser APIs.
Workbox: Simplified Event and Update Handling
Lifecycle Event Monitoring
The workbox-window module offers a clean interface for tracking critical service worker lifecycle events. It abstracts client-side APIs like updatefound and statechange behind higher-level event listeners. The code below detects installation of a new service worker version and prepares to inform the user:
const wb = new Workbox('/sw.js');
wb.addEventListener('installed', (event) => {
if (event.isUpdate) {
// Show "Update App" banner
}
});
wb.register();
Cache Change Notifications
Workbox's workbox-broadcast-update package provides a standard method for telling window clients that cached responses have changed, typically paired with the StaleWhileRevalidate strategy. To activate, add a broadcastUpdate.BroadcastUpdatePlugin to the strategy's options in the service worker:
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(),
],
})
);
Your web application then listens for these messages:
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();
}
});
Direct Browser API Strategies
For cases requiring more custom control, three browser APIs enable broadcast updates. The choice depends on your support requirements and communication complexity.
Broadcast Channel API
This API allows a service worker to create a BroadcastChannel and start posting messages. Any page can join by creating a channel on the same name. To alert pages of a new service worker, the 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 subscribes by connecting 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 straightforward but has a significant caveat: Safari doesn't currently support the Broadcast Channel API.
Client API
The Client API offers a direct path to send messages to multiple clients by iterating over Client objects. The service worker code sends 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'});
}
});
On the page side, an event handler intercepts the incoming messages:
// Listen to messages
navigator.serviceWorker.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
// Process response
}
};
The Client API is well-suited for pushing data to several active tabs, and the core API is widely supported, though you should verify the support of specific methods on your target browsers.
Message Channel
Message Channel provides a two-way port-based communication system that requires initial configuration. The page creates a channel and passes a port to the service worker using postMessage():
const messageChannel = new MessageChannel();
// Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
The page's onmessage handler on that port awaits communication:
// Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
The service worker saves a reference to the received port:
// Initialize
let communicationPort;
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'PORT_INITIALIZATION') {
communicationPort = event.ports[0];
}
});
Subsequent messages are sent via the stored reference:
// Communicate
communicationPort.postMessage({type: 'MSG_ID' });
Message Channel involves extra setup to handshake ports, but it receives support from all major browsers, making it a more appropriate choice when negotiating initial ports is necessary.



