Back to Browser Internals
Browser Internals
medium
mid

How would you keep multiple browser tabs of the same app in sync?

Options: the `storage` event (fires in OTHER tabs when localStorage changes), the BroadcastChannel API (purpose-built tab-to-tab messaging), or a SharedWorker. Common uses: sync auth/logout, theme, cart. Watch ordering, the originating tab not receiving its own event, and serialization.

4 min read·~8 min to think through

Browser tabs of the same origin are isolated, but several mechanisms let them communicate and stay in sync.

Option 1: the storage event

When localStorage (or sessionStorage) changes, a storage event fires **in all other same-origin tabs** — not the tab that made the change.

js
window.addEventListener("storage", (e) => {
  if (e.key === "theme") applyTheme(e.newValue);
  if (e.key === "auth" && !e.newValue) logout();   // another tab logged out
});
  • Pros — zero setup, widely supported, and the data is already persisted.
  • Cons — only fires in other tabs (not the originator), only triggers on actual value changes, values are strings (serialize/parse), and you're coupling messaging to storage.

Option 2: BroadcastChannel API

A purpose-built pub/sub channel for same-origin contexts (tabs, iframes, workers):

js
const channel = new BroadcastChannel("app");
channel.postMessage({ type: "logout" });
channel.onmessage = (e) => { if (e.data.type === "logout") logout(); };
  • Pros — clean API, sends structured data (not just strings), decoupled from storage, no persistence side effect.
  • Cons — ephemeral (no persistence — new tabs miss past messages), slightly less universal support (now broadly fine).

Option 3: SharedWorker

A single worker shared across all tabs — they connect to it and it relays/coordinates. Most powerful (shared state, a single WebSocket connection for all tabs) but most complex; reach for it when you genuinely need shared compute/connection, not just messaging.

Other approaches

  • Service Worker — can broadcast to all controlled clients.
  • Polling a shared store (last resort).

What you sync and the gotchas

Common: auth state / logout (log out everywhere at once), theme, cart, feature flags, "data updated, refetch."

Gotchas:

  • Originating tab doesn't get its own storage event — update its own state directly.
  • Message ordering and races between tabs.
  • Serializationstorage is strings only.
  • A "leader tab" pattern if only one tab should do something (e.g. hold the WebSocket) — elect one via BroadcastChannel/locks.
  • New tabs need initial state from storage/server, not just live messages.

The framing

"Three main tools. The storage event — fires in other tabs when localStorage changes; zero setup and the data's already persisted, but it's string-only and the originating tab doesn't get it. The BroadcastChannel API — purpose-built tab-to-tab pub/sub with structured messages, cleaner and decoupled from storage, though ephemeral. And a SharedWorker when you need genuine shared state or a single connection across tabs. Common uses are syncing logout, theme, and cart. The gotchas: the originator doesn't receive its own storage event, message ordering, and that new tabs need to hydrate initial state from storage or the server, not just live messages."

Follow-up questions

  • Why doesn't the originating tab receive its own storage event?
  • When would you use BroadcastChannel over the storage event?
  • When is a SharedWorker the right choice?
  • How do you elect a 'leader' tab?

Common mistakes

  • Expecting the storage event in the tab that made the change.
  • Forgetting storage values are strings (no auto-serialization).
  • Not hydrating new tabs' initial state — relying only on live messages.
  • Ignoring message ordering / race conditions between tabs.

Performance considerations

  • These mechanisms are lightweight, but a broadcast that triggers expensive work in every tab multiplies cost — debounce reactions, and consider a leader tab for expensive shared work like a single WebSocket connection.

Edge cases

  • A new tab opened after messages were already sent.
  • Many tabs all reacting to one event simultaneously.
  • Only one tab should act (leader election).
  • Private mode / storage disabled.

Real-world examples

  • Logging out of all tabs at once when one tab logs out.
  • Syncing theme or cart across tabs; a leader tab holding the app's WebSocket.

Senior engineer discussion

Seniors compare storage event vs BroadcastChannel vs SharedWorker with trade-offs, know the originator-doesn't-receive gotcha, handle new-tab hydration and ordering, and mention leader election for single-tab responsibilities.

Related questions