← blog
JavaScriptAugust 9, 2026 · 4 min read

Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.

Syncing state across browser tabs with localStorage events is a widely-used trick that requires JSON.stringify, event filtering, and careful cleanup. The Broadcast Channel API delivers messages between tabs directly, with none of the side effects.

Parsa Jiravand · Frontend engineer · building bestpractic
Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.

When a user logs out in one tab, the other tabs should follow. When they update their cart, every open window should reflect it. The common solution is a localStorage trick: write a sentinel value, listen for the storage event, read it, parse it, check if it's "for you," and clean it up. It works — but it's a side-channel communication pattern built on a persistence API that was never meant for messaging. The Broadcast Channel API is the direct path.

JavaScript
1
2
3
4
5
6
7
8
9
// Sender (any tab, worker, or iframe on the same origin) const channel = new BroadcastChannel('app-sync'); channel.postMessage({ type: 'LOGOUT' }); // Receiver (every other context subscribed to the same name) const channel = new BroadcastChannel('app-sync'); channel.onmessage = (event) => { console.log(event.data); // { type: 'LOGOUT' } };

Two steps: open a channel by name, then send or listen. Any tab, worker, or iframe on the same origin that opens a channel with the same name receives every message sent on it — including messages sent after they subscribed. The sender does not receive its own messages.

Close the channel when you're done to release the listener:

JavaScript
channel.close();

The typical cross-tab sync pattern using storage events:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
// Sender localStorage.setItem('__broadcast', JSON.stringify({ type: 'LOGOUT', t: Date.now() })); localStorage.removeItem('__broadcast'); // clean up immediately // Receiver window.addEventListener('storage', (event) => { if (event.key !== '__broadcast') return; // filter noise if (!event.newValue) return; // ignore the removeItem const message = JSON.parse(event.newValue); if (message.type === 'LOGOUT') { /* handle */ } });

Every part of this is load-bearing workaround: the timestamp prevents deduplication if the same value is sent twice; the removeItem triggers a second storage event that must be filtered out; JSON.stringify/JSON.parse is required because storage only holds strings. BroadcastChannel replaces the entire block with a postMessage call.

Logout across all tabs. When the user logs out, invalidate the session in every open window simultaneously:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// auth.js — runs in every tab const syncChannel = new BroadcastChannel('auth'); export function logout() { clearSession(); syncChannel.postMessage({ type: 'SESSION_ENDED' }); redirect('/login'); } syncChannel.onmessage = (event) => { if (event.data.type === 'SESSION_ENDED') { clearSession(); redirect('/login'); } };

Cart sync in an e-commerce app. Add to cart in one tab, see the count update in the header of every other tab:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const cartChannel = new BroadcastChannel('cart'); function addToCart(item) { const updated = updateLocalCart(item); cartChannel.postMessage({ type: 'CART_UPDATED', cart: updated }); renderCart(updated); } cartChannel.onmessage = (event) => { if (event.data.type === 'CART_UPDATED') { renderCart(event.data.cart); } };

Live config refresh. When an admin changes a feature flag in a settings tab, broadcast the update so every other open tab picks it up without a page reload.

BroadcastChannel uses the structured clone algorithm — the same one used by structuredClone() and postMessage() on workers. That means you can send:

  • Plain objects and arrays (including nested)
  • Date, Map, Set, ArrayBuffer, Blob
  • Primitive values — strings, numbers, booleans, null

You cannot send functions, DOM nodes, or anything not serializable by structured clone. If you try, the call throws a DataCloneError. For the message payloads most apps actually use — event objects with typed fields — structured clone covers everything without the JSON roundtrip.

BroadcastChannel is scoped to same-origin contexts — same protocol, hostname, and port. A channel named 'app-sync' on https://example.com is completely isolated from a channel with the same name on https://staging.example.com. You cannot use it to communicate between different origins.

The channel name is your namespace. If multiple features in your app use BroadcastChannel, give each a distinct name ('auth', 'cart', 'notifications') rather than sharing a single 'app' channel and multiplexing message types through it — separate channels are cleaner and don't require filtering.

BroadcastChannel is Baseline 2022: Chrome 54 (2016), Firefox 38 (2015), Safari 15.4 (March 2022). The API has been in Chromium and Firefox for nearly a decade; Safari joined in 2022. It's available in all currently-supported browser versions and in Web Workers and Service Workers, not just the main thread.

Runs right in your browser — poke at it and watch the concept react live.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

Search your codebase for storage event listeners paired with a localStorage.setItem that immediately gets removed. That pattern is cross-tab messaging through a storage side-channel — exactly what BroadcastChannel exists to replace. Swap it out: open a channel by name, call postMessage, listen with onmessage. You get structured data without serialization, no storage event noise to filter, and no cleanup sentinel to manage. The intent becomes clear in the code; the runtime handles the delivery.


Thanks for reading! Let's stay connected:

Originally published on dev.to