Area: Messaging between tabs
Related: Browser APIs (map), Web Locks API, Web Workers, IndexedDB
What is it?
BroadcastChannel is a named message bus shared by every tab, iframe and worker of the same origin. Anyone can post to a channel name, and everyone else listening on that name gets the message.
const channel = new BroadcastChannel("auth");
channel.onmessage = (event) => {
if (event.data.type === "signed-out") showSignedOutScreen();
};
channel.postMessage({ type: "signed-out" });A walkie-talkie channel for one website. Everyone tuned to “auth” hears it. Nobody outside the site can tune in.
Why it’s needed
Tabs of the same site share storage (IndexedDB, localStorage, cookies) but not memory. Each tab has its own copy of the app’s state. When one tab changes something, the others don’t know.
Typical cases:
- You sign out in one tab. The other five tabs still show your private data.
- You rename a note in one tab. Other tabs show the old name.
- A tab writes to IndexedDB. Other tabs have a stale in-memory cache.
IndexedDB has no change events, so you need a way to say “something changed”. BroadcastChannel is that way.
The details that matter
- Same origin only.
https://a.comandhttps://b.comcan’t talk. In third-party iframes, the channel is also partitioned by the top-level site, like other storage. - The sender doesn’t receive its own message. Other
BroadcastChannelobjects with the same name in the same tab do receive it. Only the exact object that posted is skipped. - Messages use structured clone, the same as Web Workers and IndexedDB. Objects, Dates, Maps, Blobs are fine. Functions throw.
- No queue and no history. If no tab is listening at the moment you post, the message is gone. A tab that opens later never sees earlier messages.
- No delivery guarantee across a crash and no reply. It’s fire-and-forget. If you need an answer, the receiver has to post a reply message with an id you sent.
- Order: messages from one sender arrive in the order they were sent. There’s no global order between different senders.
channel.close()stops listening and lets it be garbage collected. Close channels in component cleanup, or you’ll leak listeners.- Support: all modern browsers. Safari added it late (15.4). Code for older webviews should check
typeof BroadcastChannel !== "undefined".
Design rule: send “something changed”, not the data
The common mistake is sending the full new state:
// ❌ fragile
channel.postMessage({ type: "note-updated", note: fullNoteObject });Problems: the message can be large, the receiving tab may have a newer version already, messages from different tabs can cross each other, and a tab that missed a message has no way to catch up.
Better:
// ✅ better
channel.postMessage({ type: "note-changed", noteId: "n1" });
channel.onmessage = async ({ data }) => {
if (data.type === "note-changed") {
const fresh = await readNoteFromIndexedDb(data.noteId); // the real source of truth
updateUi(fresh);
}
};The message is just a ping. The receiver re-reads from the shared store. That handles out-of-order messages, duplicates, and big data for free. It’s the same idea as cache invalidation on a server: say “this key is stale”, don’t push the new value.
It also makes messages idempotent. Receiving the same ping twice just means re-reading twice.
Other ways to talk between tabs, compared
| Mechanism | How | Downsides |
|---|---|---|
| BroadcastChannel | named bus | no history, no reply |
storage event | write to localStorage, other tabs get a storage event | strings only, synchronous storage, fires only for other tabs, an old trick |
window.postMessage | send to a specific window you have a reference to | needs a handle (window.opener, an iframe); used for cross-origin talk with a target origin check |
MessageChannel | a private two-way pipe with two ports | you have to pass a port over first; great for worker RPC |
SharedWorker | one shared worker all tabs connect to | patchy support historically; more setup |
Service worker clients.matchAll() | the service worker posts to every tab | needs a service worker |
Before BroadcastChannel existed, libraries used the storage event trick: write a random value to a key, and other tabs listen for changes to that key. You’ll still see it in old code.
Combining it with Web Locks
Web Locks API decides who does something. BroadcastChannel tells everyone what happened.
Leader election example: the tab that wins the leader lock opens the single WebSocket. When a message arrives from the server, the leader posts it on a BroadcastChannel so every tab updates. When the leader closes, another tab gets the lock and takes over the socket.
Security
- Only your own origin can post, so you don’t need to check
event.originlike you do withwindow.postMessage. - Still validate the shape of messages. An old tab running last week’s code might send an older message format. Ignore types you don’t know instead of crashing.
- Don’t send secrets you wouldn’t want in other tabs. Any same-origin script, including a third-party script loaded on your page, can listen.
A real-world example
A notes app that people keep open in many tabs uses BroadcastChannel for three things:
- Sign-in and sign-out across tabs. The auth library’s account events carry the signal. Each receiving tab decides how to react: show the signed-out state, or pick up the new account.
- Local content changes. When a tab saves a note body to the local store, it posts “content changed” on a channel, so other tabs refresh their copies. It checks that
BroadcastChannelexists first. - Search index. The search worker refreshes its index when another tab reports changes, so search results stay current in every tab.
Gotchas
- You don’t get your own message. If the posting tab should also react, call the handler directly too.
- A tab opened after the message never hears it. On startup, read the current state from storage.
- Close the channel when a component unmounts.
- Don’t send full objects. Send ids and re-read.
- Messages from an older deploy may have a different shape. Ignore unknown types.
- It doesn’t cross browsers, devices or profiles. For that you need a server.
Practice
- Two tabs, one counter in
localStorage. Increment in one, update the other with BroadcastChannel. Then do the same with thestorageevent and compare. - Build “sign out everywhere”: a sign-out in any tab blanks every tab.
- Post 1,000 messages quickly from two tabs at once and check what order each tab receives them in.
- Combine with Web Locks: elect a leader that ticks a timer and broadcasts the tick to all tabs.
Sources worth reading
- MDN: Broadcast Channel API
- HTML spec: Broadcasting to other browsing contexts
- Chrome Developers: BroadcastChannel API: a message bus for the web