6 min read
0%

Broadcast Channel API for Cross-Tab Sync

Back to Blog
Broadcast Channel API for Cross-Tab Sync

Broadcast Channel API

If your web app opens in multiple tabs or windows, BroadcastChannel gives you instant same-origin pub/sub with almost no setup.

It is perfect for syncing UI state like tldraw presence, selection, or document operations between tabs.

const channel = new BroadcastChannel("tldraw-room-42");

channel.onmessage = ({ data }) => {
  if (data.type === "op") applyRemoteOp(data.op);
  if (data.type === "presence") updatePresence(data.userId, data.cursor);
};

export function publishOp(op) {
  channel.postMessage({ type: "op", op, sentAt: Date.now() });
}

window.addEventListener("beforeunload", () => channel.close());

Why BroadcastChannel

  • No server roundtrip for local multi-tab sync
  • No polling
  • No global event bus plumbing
  • Works with structured data (structured clone)

For many apps, this handles “keep tabs in sync” faster than wiring WebSocket logic.

Core Pattern for Collaborative State

Treat channel messages as operations, not full snapshots.

const channel = new BroadcastChannel("canvas-123");
const sourceId = crypto.randomUUID();
let seq = 0;

function sendOp(kind, payload) {
  channel.postMessage({
    sourceId,
    seq: ++seq,
    ts: performance.now(),
    kind,
    payload,
  });
}

channel.onmessage = ({ data }) => {
  if (data.sourceId === sourceId) return;
  applyOp(data);
};

Using sourceId and seq gives you dedupe hooks and deterministic ordering logic when needed.

Late Joiners Need State Hydration

BroadcastChannel does not keep message history. A newly opened tab only receives future messages.

Pair it with a durable source:

  • IndexedDB for operation log/snapshots
  • Server snapshot endpoint
  • localStorage for small state

Startup flow:

  1. Load snapshot/log from durable storage.
  2. Render.
  3. Start applying live channel messages.

Practical Gotchas

  • Same-origin only. Different origins cannot share a channel.
  • Sender does not receive its own message event.
  • Large payloads are cloned, which can be expensive.
  • Always call channel.close() on teardown.
  • It is not a replacement for backend sync across different devices.

Fallback for Older Browsers

If you need a fallback, use the storage event with localStorage.

const KEY = "sync-event";

function broadcastFallback(message) {
  localStorage.setItem(
    KEY,
    JSON.stringify({ ...message, nonce: Math.random(), at: Date.now() }),
  );
}

window.addEventListener("storage", (event) => {
  if (event.key !== KEY || !event.newValue) return;
  const message = JSON.parse(event.newValue);
  applyOp(message);
});

It is less ergonomic and can be noisy, but it works as a compatibility path.

tldraw-Style Setup

A reliable browser-local collaboration stack often looks like:

  1. In-memory store for live canvas state.
  2. BroadcastChannel for cross-tab operations and presence.
  3. IndexedDB for durability and crash recovery.
  4. WebSocket/WebRTC only when you need cross-device realtime.

That split keeps local collaboration fast while leaving network sync as a separate concern.

Browser Support

Broadcast Channel API has broad support in modern browsers. Use feature detection:

const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";

If unavailable, downgrade to storage-event sync or disable multi-tab realtime features gracefully.


Browser support snapshot

Live support matrix for broadcastchannel from Can I Use.

Show static fallback image Data on support for broadcastchannel across major browsers from caniuse.com

Source: caniuse.com

Canvas is not supported in your browser