The need for cross-tab communication is common in scenarios where there is a lot of content to browse or manipulate (e.g. blogs or CMS systems) and data consistency needs to be maintained. Most of these scenarios can be solved by bringing in a back-end service to keep the data consistent across multiple tabs by polling and so on. This article describes a pure front-end solution for cross-tab communication without introducing back-end services

Cookies & IndexedDB

To achieve consistency in data, it is easy to think of a unified data store. When there is a backend on board, the backend is really a unified data store for the frontend. In a pure front-end environment, the more common data stores include

sessionStorage cannot be used for cross-tab communication as it only lives in a single tab session. localStorage has other applications, which will be analysed in depth below. So in this section we will focus on cookies and IndexedDB.

Implementation idea

The implementation is really quite simple - poll the data source to ensure that the data in each tab is up to date.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Cookie Way
// Producer Tab, 当然你也可以用js-cookie这种库更高效的操作cookie
document.cookie = "blabla"

// Consumer Tab
setInterval(() => {
  const cookie = document.cookie;
  //Use Cookie to do something
  doSomething(cookie)
}, 1000)

// IndexedDB way
// Producer Tab
const transaction = db.transaction(["customers"], "readwrite");

const objectStore = transaction.objectStore("customers");
customerData.forEach(function(customer) {
  const request = objectStore.add(customer);
  request.onsuccess = function(event) {
    // do some callback
  };
});

// Consumer Tab
setInterval(() => {
  const transaction = db.transaction(["customers"]);
  const objectStore = transaction.objectStore("customers");
  const request = objectStore.get("444-44-4444");
  request.onsuccess = function(event) {
    doSomething(request.result.name);
};
}, 1000)

It is important to note that cookies are read and written synchronously and may block the main thread when the cookie is large causing the page to lag. The IndexedDB API is asynchronous, so there are no performance issues with cookies being read or written synchronously.

The idea of using a unified data source for cross-tab communication is relatively simple, but polling is inefficient when data changes are infrequent (essentially a consumer pull). Therefore, it would be better to use events to drive the response from the consumer (in the form of a producer push).

localStorage Event

image

LocalStorage is also essentially a data source, but unlike the two data sources mentioned above, when the contents of localStorage change, a corresponding event is raised and all tabs under the same source can receive the event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Producer Tab
window.localStorage.setItem("foo", "bar");

// Consumer Tab
window.addEventListener('storage', (event) => {
 if (event.storageArea != localStorage) return;
 if (event.key === 'foo') {
   // Do something with event.newValue
 }
});

Of course, there are some disadvantages to this approach, which need to be noted when using

  • Similar to cookies, localStorage reads and writes are synchronous, so when the amount of storage data is large, reading and writing may itself block the main thread and cause the page to lag.
  • The value of localStorage only supports string, which is a bit troublesome when passing complex data.
  • The page that triggers setItem will not receive the storage event (FYI: https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent).

Broadcast Channel

image

Broadcast Channel is a web API that enables communication across tabs, windows, frames, iframes and even workers from the same source, and unlike localStorage, the message object sent can be of any Object type, making it ideal for cross-tab communication.

1
2
3
4
5
6
7
8
// Producer Tab
// init channel
const bc = new BroadcastChannel('test_channel');
// send message
bc.postMessage('This is a test message.');

// Consumer Tab
bc.onmessage = function (ev) { doSomething(ev); }

The downside of Broadcast Channel is its browser compatibility. As of this writing, Safari does not support Broadcast Channel (FYI https://caniuse.com/?search=broadcast).

ServiceWorker

The more common scenario for ServiceWorker is probably caching data and loading data to improve page loading speed. However, ServiceWorker itself also supports sending messages and can therefore be used to communicate across tabs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Producer Tab
navigator.serviceWorker.controller.postMessage({
 broadcast: data
});

// Consumer Tab
addEventListener('message', async (event) => {
 if ('boadcast' in event.data ) {
  const allClients = await clients.matchAll();
  for (const client of allClients) {
   client.postMessage(event.broadcast);
  }
 }
});

The downside of using ServiceWorker for cross-tab communication is that you need to learn and understand ServiceWorker, and if your site doesn’t use ServiceWorker in the first place, using it just for cross-tab communication might be putting the cart before the horse.

window.postMessage

If you want to communicate across tabs from different sources, you will have to use window.postMessage.

1
2
3
4
5
6
7
8
9
// Producer Tab
targetWindow.postMessage(message, targetOrigin)

// Consumer Tab
window.addEventListener("message", (event) => {
  if (event.origin !== targetOrigin)
    return;
  // Do something
}, false);

Summary

  • Cross-tab communication is simple with cookies or IndexedDB, but polling performance needs to be considered
  • LocalStorage Event can be used to transfer across tabs, but performance and some boundary scenarios need to be considered.
  • If there are no browser compatibility requirements, Broadcast Channel is probably the best choice.
  • ServiceWorker can also communicate across tabs, but you need to evaluate the cost of introducing ServiceWorker in advance
  • If you need to communicate across tabs across domains, you can only choose window.postMessage