Reduce WebSocket Connections With Shared Workers

websockets_blog.jpg

Using Pusher within a shared worker, we can keep only one websocket connection per browser window. That way, if your users open your app in multiple tabs, you can keep your connection count low. This blog post will give you a step-by-step guide on how to get set up.

Introduction

A question frequently asked in support is how to cut down on your concurrent Pusher connections and avoid any limits associated with your plan. Given that a new connection to our API is created whenever your page is loaded in a new tab – it would be massively beneficial to customers if they could share a single connection across multiple tabs. The solution? Using Pusher within a shared worker, we can keep only one websocket connection per browser window. That way, if your users open your app in multiple tabs, you can keep your connection count low. This blog post will give you a step-by-step guide on how to get set up. If you get stuck at any point, feel free to consult our example on Github.

Recently, our own Paweł Ledwoń blogged about how we wrote the isomorphic version of PusherJS, opening up our API to new Javascript environments. Other than NodeJS and React Native, one of the awesome new ways you can use PusherJS is in browser workers.

For those who are new to the area, a web worker is essentially Javascript code that runs in a separate thread, independent of other scripts and entirely isolated from the DOM.

It is currently impossible to use official PusherJS in a worker, as the library uses the DOM for JSONp and loading dependencies such as XHR fallbacks and SockJS. However, with the experimental pusher-websocket-iso, the dependency on the DOM is removed, thus letting customers use Pusher within this new environment.

The solution? Using Pusher within a shared worker, we can keep only one websocket connection per browser window. That way, if your users open your app in multiple tabs, you can keep your connection count low. This blog post will give you a step-by-step guide on how to get set up. If you get stuck at any point, feel free to consult our example on Github.

Getting Started

Let’s make a tiny app that simply connects to Pusher and shows any incoming messages. We’ll be looking at new connections being made and messages being sent on our dashboard debug console.

While you’re on the dashboard, go ahead and fetch your app key. We’ll use this to connect to Pusher, as per usual.

Download the worker distribution of pusher-websocket-iso and call it something like "pusher.worker.js". Create an "index.html" file for your web page, and create a file called "shared_worker.js" for your worker code.

Inside The Web Page

All we’re going to have on this page is an element to display incoming messages, and a button to disconnect the page from Pusher.

1<!DOCTYPE html>
2<html>
3<head>
4  <title>Pusher + Shared Workers</title>
5  <script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
6</head>
7<body>
8
9  <button id="disconnect">Disconnect</button>
10
11  <div>
12    <p>Message: <span id="message"></span></p>
13  </div>
14
15</body>
16</html>

Now create a script tag in which we’ll initialize our worker script. We’ll first want to check that the browser can support the SharedWorker API:

1if (typeof(window.SharedWorker) === 'undefined') {
2  throw("Your browser does not support SharedWorkers")
3}
4
5var worker = new SharedWorker("./shared_worker.js");

In order to receive messages from our worker, we’ll have to bind its port, and set an onmessage function. In this our case we can just stringify the object we receive and render it to the #message element. Also, if an error occurs in the worker, let’s just log out the message and close the worker:

1worker.port.onmessage = function(evt){
2  console.log(evt.data);
3  $('#message').text(JSON.stringify(evt.data));
4};
5
6worker.onerror = function(err){
7  console.log(err.message);
8  worker.port.close();
9}

Then we can the worker by opening the port:

1worker.port.start();

Inside The Worker

In order to import "pusher.worker.js" we can use the special worker-specific importScripts function. Then, as usual, instantiate our Pusher object with our app key, and subscribe to our channel, in this case "test_channel".

1importScripts("pusher.worker.js");
2
3// Connect to Pusher
4var pusher = new Pusher('1fb94680701ab31a3139', {
5  encrypted: true
6});
7
8// Subscribe to test_channel
9var pusherChannel = pusher.subscribe('test_channel');

Before we listen for any particular event, we first need to keep track of all the browser tabs connected to our shared worker. Whenever a tab connects, the worker receives a built-in "connect" event, and we can just push the tab’s port to an array of clients. Then we open the connection by starting the port:

1// An array of the clients/tabs using this worker
2var clients 
3
4self.addEventListener("connect", function(evt){
5
6  // Add the port to the list of connected clients
7  var client = evt.ports[0];
8  clients.push(client);
9
10  // Start the worker.
11  client.start();
12});

Now that we can track the connected tabs in the worker state, all we have to do now is bind to a Pusher event on pusherChannel and communicate the data onto to all clients. In the world of workers, the way we communicate between a web-page and a worker is by sending messages. This is achieved by a simple postMessage function:

1// bind to 'my_event' on pusherChannel
2pusherChannel.bind('my_event', function(data) {
3
4  // Relay the payload on to each client
5  clients.forEach(function(client){
6    client.postMessage(data);
7  });
8});

Voilà, we have our web-page and our worker set up. A visitor will visit the page, which will open a connection with the Shared Worker. This worker will relay any incoming Pusher messages to the web page, which will simply stringify the payload and show it on a web-page. Let’s look at what the outcome is.

The Magic

Open up the Pusher debug console on your dashboard. Here you will be able to see any new connections, and send messages to your channels.

In a separate window, open up your index.html file. In your debug console, you should see one connection created. Expand the Event Creator and send an event called my_event on test_channel, with whatever payload you wish. You should now see it on the DOM.

Now, open a new tab. You should see on your dashboard that no new connection has been made. Send another event, and both tabs should display the message.

Job done! Now you have a little app that lets you cut down on your number of connections by sharing them across tabs.

Let Us Know How You Get On!

Feel free to tweet us and let us know if you’re planning to use this technique in building your realtime apps. I am personally really excited about the experimental isomorphic Javascript library, and would thoroughly welcome any new contributions. Obviously it’s still unofficial, as it’s only suited for modern browsers that support XHR and don’t need JSONp, but I’m eager to hear suggestions for new environments that we could open the client up to. Equally, if you’re building apps with any other types of web workers, React Native, or Electron, we’d love to know!