How to communicate with Service Workers

MessageChannel, Broadcast API and Client API compared for using a two-way communication

How to communicate with Service Workers Thumbnail

Service Workers are great. They allow web developers to implement native-like features that before were exclusive to native applications. Such features are for example push notifications or background syncs for offline capabilities.

They are the core of progressive web apps. But after setting them up it seems to be difficult to do more complex things that involve interaction with your web application.

In this post, I'll showcase the options that are available and compare them in the end.

If you're new to Service Workers, I suggest you check out my introduction article on Service Workers, where I explain the basic concepts and features.

Table of Contents

Service Workers vs Web Workers

If you look up the API of Service Workers you will see that Web Workers and Service Workers actually have very similar interfaces. But despite their similarities, their intent and capabilities are very different:

Service Worker vs Web Worker

  • Service Workers can intercept requests and replace them with items from their own cache, thus they behave like a proxy server. They offer offline capabilities to web applications.

    They can be used across multiple tabs and even continue to be alive when all tabs are closed.

  • Web workers, on the other hand, have a different purpose. They offer multi-threading to the single-threaded JavaScript language and are used for performing computation heavy tasks that should not interfere with the responsiveness of the UI.

    They are limited to only one tab.

Both of them have in common that they don't have access to the DOM and communicate using the postMessage API. You can think of them as Web Workers with expanded capabilities.

If you want to learn more about these two, check out this talk which, event though is a little old, gives a good overview of this topic. Being 2020, the browser support of Service Workers has improved a lot.

How to talk to Service Workers

Select the Service Worker you want to send a message to

For any origin, it is possible to have multiple Service Workers. The following returns the active Service Worker that currently controls the page:

navigator.serviceWorker.controller

If you want to access other Service Workers you can access them through the registration interface, which gives you access to Service Workers in the following states:

  • ServiceWorkerRegistration.installing
  • ServiceWorkerRegistration.waiting - This Service Worker is installed but not active yet
  • ServiceWorkerRegistration.active - This Service Worker is controlling the current page

You can get access to the registration interface in a few different ways. One of them is calling navigator.serviceWorker.ready. This returns a promise that resolves with a registration:

navigator.serviceWorker.ready.then((registration) => {
  // At this point, a Service Worker is controlling the current page
});

Have a look at this article if you want to learn more about the lifecycle. of Service Workers.

Send the message

As I already mentioned, Service Workers communicate through the postMessage API. This doesn't only allow them to exchange data with the JavaScript main thread, but it's also possible to send messages from one Service Worker to another.

// app.js - Somewhere in your web app
navigator.serviceWorker.controller.postMessage({
  type: 'MESSAGE_IDENTIFIER',
});
// service-worker.js
// On the Service Worker side we have to listen to the message event
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MESSAGE_IDENTIFIER') {
    // do something
  }
});

A use case for this one-way communication would be to call skipWaiting in the waiting Service Worker, which will then pass on to become active and control the page. This is already implemented in the Service Worker that is shipped with Create-React-App. I used this technique for showing an update notification in a progressive web app, which I explain in this post.

But what if you want to send a message back to the Window context or even to other Service Workers?

Service Worker - Client Communication

In this section I'll go over the options you have to send messages back to the client, as part of the two-way communication. There are a few ways of sending messages to the clients of a Service Worker:

  • Broadcast Channel API, which allows communication between browsing contexts. This API allows communication between contexts without a reference.

    This is currently supported for Chrome, Firefox, and Opera. Set's up a many to many broadcast communication.

  • MessageChannel API

    It can be used to set up a 1-to-1 communication between the Window and the Service Worker context.

  • The Clients interface of the Service Worker

    It can be used for broadcasting to one or more clients of the Service Worker.

I'll give you a short example of each of them and then compare them to each other to see which one might be the best for your use case.

I didn't include FetchEvent.respondWith() since this only applies to fetch events and is currently not supported by Safari.

Using the MessageChannel API

As the name already tells us, the MessageChannel API sets up a channel through which messages can be sent.

The implementation can be boiled down to 3 steps.

  1. Set up event listeners on both sides for the 'message' event
  2. Establish the connection to the Service Worker by sending the port and storing it in the Service Worker.
  3. Reply to the client with the stored port

A fourth step could be added if we want to close the connection by calling port.close() in the Service Worker.

In practice that looks something like this:

// app.js - somewhere in our main app
const messageChannel = new MessageChannel();

// First we initialize the channel by sending
// the port to the Service Worker (this also
// transfers the ownership of the port)
navigator.serviceWorker.controller.postMessage({
  type: 'INIT_PORT',
}, [messageChannel.port2]);

// Listen to the response
messageChannel.port1.onmessage = (event) => {
  // Print the result
  console.log(event.data.payload);
};

// Then we send our first message
navigator.serviceWorker.controller.postMessage({
  type: 'INCREASE_COUNT',
});
// service-worker.js
let getVersionPort;
let count = 0;
self.addEventListener("message", event => {
  if (event.data && event.data.type === 'INIT_PORT') {
    getVersionPort = event.ports[0];
  }

  if (event.data && event.data.type === 'INCREASE_COUNT') {
    getVersionPort.postMessage({ payload: ++count });
  }
}

Using the Broadcast API

The Broadcast API is very similar to the MessageChannel but it takes away the need to pass the port to the Service Worker.

In this example, we see that we just need to set up a channel on both sides with the same name count-channel.

We could add the same code to other WebWorkers or Service Workers who will receive all those messages then as well.

Here we see the same example from above but with the Broadcast API:

// app.js
// Set up channel
const broadcast = new BroadcastChannel('count-channel');

// Listen to the response
broadcast.onmessage = (event) => {
  console.log(event.data.payload);
};

// Send first request
broadcast.postMessage({
  type: 'INCREASE_COUNT',
});
// service-worker.js
// Set up channel with same name as in app.js
const broadcast = new BroadcastChannel('count-channel');
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'INCREASE_COUNT') {
    broadcast.postMessage({ payload: ++count });
  }
};

Using the Client API

The Client API also doesn't require to pass a reference to the channel.

On the client-side, we listen to the response of the service worker and in the Service Worker we select the client we want to send the response to with the filter options that the self.clients.matchAll function provides us.

// app.js
// Listen to the response
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'REPLY_COUNT_CLIENTS') {
    setCount(event.data.count);
  }
};

// Send first request
navigator.serviceWorker.controller.postMessage({
  type: 'INCREASE_COUNT_CLIENTS',
});
// service-worker.js
// Listen to the request
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'INCREASE_COUNT') {
    // Select who we want to respond to
    self.clients.matchAll({
      includeUncontrolled: true,
      type: 'window',
    }).then((clients) => {
      if (clients && clients.length) {
        // Send a response - the clients
        // array is ordered by last focused
        clients[0].postMessage({
          type: 'REPLY_COUNT',
          count: ++count,
        });
      }
    });
  }
});

Conclusion

The postMessage API offers a simple and flexible interface that allows us to send messages to Service Workers.

The Broadcast Channel API is the easiest-to-use option to respond to the client, but unfortunately, does not have very good browser support.

From the remaining two I like the Client API better since this doesn't require passing a reference to the Service Worker.

If you want to know more about PWAs, I recommend you check out my complete guide to Progressive Web Apps with React or my other articles on PWAs.