PWA patterns for window and service worker communication

Dec 22, 2020 20:00 · 3200 words · 16 minute read similarities fetch type look ideas

(upbeat music) - Hi, Andrew, how’s it going? - Hey, Demian. I’m doing well. What do you have for audience today? - Today we’re going to talk about workers and in particular, two types of them, web workers and service workers. Their similarities, differences, the most popular communication APIs, and some common use cases. What about you Andrew? - I’ll have some real world examples of how these patterns are used by site and Web apps for speed, reliability, and better user experience. - Great. Let’s dive right into it. To begin, let’s start with a quick recap on multithreading in the Web.

00:45 - The browser uses a single thread, the main thread, to run all the JavaScript in a Web page as well as to handle user inputs and events and things like rendering the page and performing garbage collection. In platforms like Android and iOS, there’s also the concept of a main thread also called the UI thread. Logging the main thread for too long can result in warnings and errors. For example, in the latest versions of Android, not responding to event for five consecutive seconds is not only bad for the user experience but it directly produces an application crash. The web is not a straight designated platform in this aspect, but occupying the main thread for long periods of time certainly has an impact in the user experience.

01:35 - We recently introduced the concept of Core Web Vitals, a set of quality signals to achieve great user experiences in the Web. Expansive JavaScript code can lead to more users perceiving some of these metrics as poor, especially in resource constrained devices. For example, running blocking JavaScript at the beginning of a page load can delay rendering leading to a slower Largest Contentful Paint. But perhaps the aspect that could suffer the most is interactivity. Blocking the main thread can prevent the browser from receiving and processing user events, leading, for example, to higher First Input Delay.

02:17 - Maybe publications also offer ways of executing long running task by heavy downloads in the background with independence of the user interface. In the Web, running that code directly in the page might not only block the main thread but will also interrupt the task if the tab is closed or the user navigates away from the page. Unlike native app languages, JavaScript was designed around the concept of a single thread and lacks capabilities needed to implement a multithreading model like the ones that apps have. It’s by these, a similar pattern can be achieved in the Web by using workers, which the way of running scripts in background threads without interfering with the user interface. We have talked several times about two types of workers, web workers and service workers.

03:09 - They not only sound similar but they actually have some things in common. First, both run in a secondary thread allowing you to execute JavaScript code without blocking the UI. Second, they don’t have access to the Window or document objects. Therefore they can’t interact with the DOM directly. and have limited access to browser APIs. Third, they can communicate with the page via asynchronous communication APIs.

03:38 - Finally, both types of workers are currently supported by all major browsers. Due to these similarities, one might think that anything that can be done in a web worker could be done in a service worker, but there are important differences too. The first is that the service worker can listen for certain specific events like fetch to intercept network request and push events in the background via the push listener. These are not tasks for the web worker. The second has to do with the relationship with the page. While any source of a web worker belongs to a single page, a page can span multiple web workers, for example, image compression Web app Squoosh uses two web workers to apply independent optimizations to each side of an image without blocking the UI.

04:31 - Service workers are the opposite in this aspect. A page or tab is controlled at most by a single service worker, but the service worker can control multiple active tabs for a given scope. The final difference has to do with their life span and what worker is tightly coupled to the tab it belongs to. For that reason, closing the tab where the web worker is running will terminate the web worker as well. In the case of a service worker, their lifespan is independent.

05:03 - Closing all its active tabs might not mean that the service worker will get killed. This allows the service worker to continue running tasks in the background with independence of the user interface. API slide background fetch for example let the service worker process long task like large media downloads in the background without the need of any active tabs. The difference between both types of workers suggests in which cases one might want to use one or the other. Use cases for web workers are more commonly related to offloading work.

05:38 - A heavy computations to a secondary thread to avoid blocking the UI. Video games like Proxx use web workers to upload expansive game law sheet to a separate thread to achieve a more responsive user interface. Service worker tasks are generally more related to acting as a network proxy and things like caching and offline. In a podcast (indistinct) for example, one might want to allow users to download complete episodes to listen to them while offline. These tasks can be delegated to a service worker, that way if the user closes a tab while the episode is downloading, the task doesn’t have to be interrupted.

06:19 - Before moving into production examples, a final aspect to consider about these two types of workers is how they can communicate with the page. At the higher level, communication always follows the same pattern. One context calls the other via the postMessage interface. The other end implements an onmessage handler to receive and process the request. And the same pattern can be applied in the opposite direction.

06:47 - As we’ll see, depending on the type of worker, there are more specific APIs on top of these methods to let you implement the most common use cases. Let’s start with communication with web workers. As we have learned, an instance of a web worker is tightly coupled to a single page or tab. For that reason, implementing two-way communication with the page is relatively straightforward. First, the page creates a worker object and communicates with it via postMessage. The worker implements an onmessage listener to receive and process the message. To communicate back with the page, it can directly go postMessage and send the response. Finally, the page implements an onmessage listener to receive the response from the web worker. Window and service worker communication can be more complex. For the page, identifying the service worker associated with it is not as complicated because a path or route can be controlled at most by single service worker.

07:55 - But as we have seen, one service worker can control multiple active tasks at the same time. So it needs to determine first, to which client to communicate to. There are different APIs that can be used to that end. In this talk, we’ll cover three of them. Broadcast Channel, Client and Message Channel. The first API is Broadcast Channel which allows basic communication between browsing contexts like tabs, frames, web workers and service workers.

08:28 - In this API, each context subscribes to a channel by instantiating a BroadcastChannel object with a given ID. Messages are sent and received via the interface that Broadcast Channel provides. To call is the same for any context like pages and service workers. First, instantiate a BroadcastChannel object with a given ID. To send messages to the other context, call postMessage on the BroadcastChannel object. You can optionally use a message ID to identify the type of message or operation that you’re invoking. Similarly, to listen to messages, implement an onmessage listener on the broadcast object. As we have seen, Broadcast Channel API is a simple way of implementing one way communication without referring explicitly to any particular context. At the moment of this talk, this API is supported by most of the major browsers except Safari. Our next API is the Client API. This API provides an interface to the service worker to obtain an array of all the active tabs, order by the Last Focused tab.

09:44 - That way, the service worker can iterate and communicate directly with any client. For pages, communicating with the service worker is simple. They can call postMessage in the service worker interface. Similarly, they can listen to messages by implementing an onmessage listener on the service worker interface. To communicate back with the page, the service worker first obtains an array of all the active clients. Then it can send messages using the postMessage method on each client object. In this example, we send a message to the Last Focused tab which can be accessed from the first position of the array. As you can see, the client API makes it really easy to communicate with different tabs. In fact, this is one of the techniques that Surma and Jake recently explored as a way of getting multiple active tabs synchronized through a service worker. Client API is available in most browsers but not all its methods are.

10:50 - So make sure to check browser support before using a particular functionality. Our final API is Message Channel. This API requires an initial configuration step to define and pass a port from one context to another, to establish a two-way communication channel. The page instantiates a MessageChannel object and uses postMessage to send the port to the register serviceWorker. The service worker receives the message and saves a reference to the port. Later, it can communicate with the page by calling postMessage through the reference of the port.

11:31 - Finally, the page can listen to messages by implementing a onmessage handler in the MessageChannel object. Message Channel is the only API that comes with support from all the major browsers. So, as a quick recap, we have seen three ways of implementing communication between context. Each of them use the postMessage interface and offer more or less flexibility depending on the use case. Taking into account that support come by across browsers.

12:03 - So make sure to check if an API or method is available before implementing a functionality in your side. These APIs can help you implement Window and service worker communication in many cases, but they’re even more specific APIs for certain scenarios. For example, background sync and background fetch. Background sync lets you defer actions until the user has stable connectivity. This is useful for ensuring that whatever the user wants to send is actually sent.

12:36 - For example, Google Search uses this API to retrieve fake worries and inform the results to the user via post notifications. Background fetch is another advanced way of calling the service worker. This API is useful if you want to download something that might take a long time like in movie, podcast or levels of a game. A final suggestion around communication. As you might expect, there are libraries that can be used to abstract many details of this process and help you implement these patterns more easily. For web workers, Comlink is the library that the examples we saw like Proxx and Squoosh use.

13:19 - For service workers, Workbox contain some modules that can help you implement the most common use cases. In the rest of this talk, we’ll focus mostly on cases for page and service worker communication. But if you want to know more about web workers, checkout Summa’s talk at Chrome Dev Summit 2019. Next, Andrew will share some patterns and real world examples of page and service worker communication with a focus on reliability, speed and other use cases. - Thank you Demian. In this section, we’ll take a look at the communication patterns and their real world examples.

13:57 - The first pattern is Window to service worker communication. For this pattern, a common use case is imperative caching. With imperative caching, the page can try to accomplish two things. One, to fetch the assets ahead of time by anticipating which link to use or one to click on. And two, to enable the content on the current page to be accessible even when the device is offline. To implement this pattern, we’ll use the postMessage API. This powerful API allows sending and receiving of texts, blobs, or even memory objects as a message between the Window and the service worker. On the page, we can curate a list of URLs to be imperatively cached based on certain conditions, such as the scroll position on the page or the links visible inside the viewport. Then we’ll pass the list over to the service worker to be fetched and stored in the browser’s cache. This allows the page to offload heavy lifting or fetching and caching to the service worker, freeing up the main thread to handle more important tasks such as user inputs.

15:09 - By doing this, we actually achieve parallel computing in the browser. Pretty awesome, isn’t it? Let’s take a look at a real life example of this patten. 1-800-Flowers.com, a very popular floral and gift e-commerce site here in the United States, uses service worker to imperative the cache product data to speed up user navigations. When a product listing page such as this one is loaded, the top 10 pro items on the list are sent to the service worker for pre-fetching. Also, whenever the user mouse is over a product item, the page will ask the service worker to fetch the data for that item.

15:51 - Upon receiving the product IDs from the page, the service worker will fetch those products JSON files over the network. So when the user eventually clicks on one of those pre-fetched items, the data will be available locally already in the browser’s cache for immediate consumption. Here’s how to implement the similar behavior of imperative caching. On the page, first we want to make sure the service worker is activated and ready to receive messages. Then we’ll use the postMessage API to send a message.

16:27 - Now, the following part is specific to the imperative caching purpose. Inside the message, we put the list of URLs in the JSON object along with a pre-fetch type to indicate that the intent is to ask the service worker to pre fetch these URLs. On the service worker side, first we create an event listener to handle messages sent from any pages in this service worker scope. Then, if the message is an instruction to pre fetch, we iterate through the list of URLs. Here, we use a helper function called Fetch Async to encapsulate the operation.

17:08 - The next pattern is service worker to Window communication. A common use case is to broadcast updates that take place on the service worker side. For example, to inform the pages when a newer version of the service worker has been installed, then the pages can take action so that the user can benefit right away from the updates, or to let the user know that certain content has been cached locally for offline access. The Web apps capability to work offline is powered by it’s service worker. Therefore, when the service worker is activated and has downloaded all the necessary assets to be used offline, it notifies the Web app.

17:54 - Upon notification, the Web app can subsequently display a pop-up, informing the user that it’s ready to be used offline. Let’s take a look at another example. Tinder, a populist geo social networking and online dating site here in the US and worldwide, also utilizes this service worker to Window communication pattern. When there’s a newer version of the service worker activated, the Tinder Web app will be notified by the service worker and displays a button to encourage the users to update. One way to achieve the same user experiences in the examples is using the Broadcast Channel API as Demian mentioned earlier. In the service worker code, we can create a BroadcastChannel object with a channel ID that would be listened to by the subscribing pages. And then we post messages to that object.

18:54 - Inside the message, we’ll send an instruction type called Service Worker Updated. In the page code, we also create a BroadcastChannel object with the same ID. Incoming messages can be handled by the onmessage event handler. Here, since the instruction in the message received is service worker updated, we’ll show a prompt to the user asking to refresh. Combining the previous two patterns, we’ll come to our final pattern of bidirectional communication.

19:28 - A well designed bidirectional communication allows the service worker to be used to its full potential by taking the advantage of its parallel processing nature and its independent lifespan. Imagine this podcast Web app in which the user chooses episodes to download and listen to. Because of the file size can be large, downloading directly on the page may block all the important tasks on the main thread. And if the user closes the app before the download is completed, it will have to start over again when the user comes back. But if we delegate downloading to the service worker, both problems can be solved.

20:08 - Whenever an episode is selected, the Web app can instruct the service worker to fetch the episode. While the service worker is helping per request, the main thread can take care of other things. And if the app is closed prematurely, the service worker can still finish the job in the background because it is still within the service worker’s lifespan. Once the fetching is completed, the service worker can then message the app to take action. For example, changing the icon so that the user can stop playing the episode.

20:43 - This pattern can be further enhanced by the background fetch API in which even the service worker can further delegate to download to the operating system. With the operating system handling the download, the user will be given more control, such as pausing, resuming, and canceling the process outside of the app. In this talk, we explored the differences between a web worker and a service worker, how a service worker can help improve UX, to communication patterns between Window and service worker, and the real-world examples of how these patterns can be applied. There are endless possibilities to make the service worker dance nicely together with your site or your Web app. Give these ideas a try, take them to the next level and let us know about your innovative ways of using it. Thank you and see you next time. (upbeat music) .