12k
All articles

Browser-Tab-Synchronisierung mit BroadcastChannel

BroadcastChannel synchronisiert Tabs in Echtzeit mit Structured-Clone-Nachrichten, Cleanup, Fallback und Mustern für Auth, Warenkorb und Theme.

OpenReplay Team
OpenReplay Team
Browser-Tab-Synchronisierung mit BroadcastChannel

Die BroadcastChannel API ist ein nativer Browser-Nachrichtenbus, der Tabs, Fenster, iFrames und Worker derselben Origin in Echtzeit miteinander kommunizieren lässt — die zweckgerichtete Lösung für Multi-Tab-Zustandsdrift, bei der ein Logout in einem Tab die anderen weiterhin mit einer eingeloggten Benutzeroberfläche anzeigt. Es handelt sich um eine API mit vier Aufrufen, ohne Server, ohne Handshake und ohne Abhängigkeiten. Dieser Artikel behandelt die minimale API, das aktionsbasierte Nachrichtenmuster für skalierbare Implementierungen, die Framework-Integration mit korrektem Cleanup, die nicht offensichtlichen Fallstricke sowie die aktuelle Browser-Unterstützung mit einem ausführbaren Fallback.

Wichtigste Erkenntnisse

  • Die gesamte API besteht aus vier Aufrufen — new BroadcastChannel(name), postMessage(data), onmessage und close() — ohne Server, Handshake oder Konfiguration.
  • Da postMessage den Structured-Clone-Algorithmus verwendet, können Objekte, Maps, Sets und Blobs direkt übertragen werden — kein JSON.stringify, kein manuelles Parsen auf der Empfängerseite.
  • Ein Tab empfängt niemals seine eigenen Broadcasts: Nachrichten werden an jeden lauschenden Kanal gesendet, außer an das Objekt, das sie gesendet hat, was Rückkopplungsschleifen verhindert, gegen die man sich andernfalls manuell absichern müsste.
  • BroadcastChannel ist eine Pipe, kein Speicher — er transportiert Nachrichten, speichert jedoch nichts. Kombinieren Sie ihn daher mit localStorage oder IndexedDB für Persistenz und zur Initialisierung von Tabs, die nach dem Auslösen eines Ereignisses geöffnet werden.
  • BroadcastChannel ist Baseline und seit März 2022 in Chrome, Firefox, Edge, Safari, Opera und Samsung Internet weitgehend verfügbar; die Safari-Unterstützung wurde in Version 15.4 eingeführt. Nur der Internet Explorer unterstützt ihn nicht.

Was ist Multi-Tab-Zustandsdrift?

Multi-Tab-Zustandsdrift bezeichnet eine Klasse von Fehlern, bei denen zwei geöffnete Tabs derselben Anwendung voneinander abweichende Zustände aufweisen: Man meldet sich in einem Tab ab, während der andere weiterhin das Dashboard anzeigt, oder man leert einen Warenkorb in einem Tab, während der andere die Artikel noch anzeigt. Session-Replays solcher Abläufe zeigen regelmäßig das sichtbare Symptom — ein Nutzer, der eine Bestellung gegen einen anderswo geleerten Warenkorb abschickt, oder der in einem Tab weitermacht, der eigentlich ausgeloggt sein sollte — genau diese Desynchronisierung beseitigt BroadcastChannel.

Entwickler haben dieses Problem bisher mit drei unzulänglichen Workarounds gelöst. Das storage-Event von localStorage wird zwar tab-übergreifend ausgelöst, überträgt jedoch nur Strings, was bei jeder Nachricht Serialisierung und Parsing sowie umständliches Key-Management erfordert. Das Polling eines Servers oder von localStorage in Intervallen ist ressourcenintensiv und träge — man zahlt für Prüfungen, die meistens nichts finden. SharedWorker und WebSockets sind zwar echte Werkzeuge, aber ein Socket-Round-Trip über einen Server, nur um zwei Tabs auf demselben Gerät zu synchronisieren, ist für ein rein lokales Problem zu aufwendig.

MechanismusPayload-TypPersistiert?Netzwerk erforderlichGeeignet für
BroadcastChannelStructured Clone (Objekte, Maps, Blobs)NeinNeinLokale same-origin Tab-/Worker-Synchronisierung
storage-EventNur StringsJa (localStorage)NeinEinfache Synchronisierung mit eingebauter Persistenz
SharedWorkerStructured CloneNeinNeinGemeinsame Berechnung/Verbindung über Tabs hinweg
WebSocketStrings/BinärServerseitigJaLive-Server-Push-Daten über Clients hinweg

Die BroadcastChannel API in vier Aufrufen

Die gesamte API besteht aus vier Aufrufen, und jeder Kontext, der einen Kanal mit demselben Namen erstellt, tritt demselben Bus bei. Man verbindet sich, lauscht, sendet und schließt:

const channel = new BroadcastChannel("app-sync");

channel.onmessage = (event) => {
  console.log("Received:", event.data);
};

channel.postMessage({ hello: "world" });

channel.close(); // beim Unmount / Teardown

Der Payload ist der entscheidende Vorteil. Über postMessage gesendete Daten werden mit dem Structured-Clone-Algorithmus serialisiert, sodass Objekte, Arrays, Map, Set und Blob direkt ohne Stringifizierung übergeben werden können — der Empfänger liest event.data als fertiges Objekt. Symbols und SharedArrayBuffer sind die nennenswerten Ausnahmen, die nicht geklont werden können. Kanäle sind auf dieselbe Origin beschränkt, und die Wiederverwendung einer einzelnen Kanalinstanz pro Kontext ist wichtig: Das Erstellen eines neuen BroadcastChannel bei jedem Sendevorgang führt zu Listener-Leaks.

Wie sollten BroadcastChannel-Nachrichten strukturiert werden?

Das skalierbare Muster ist eine diskriminierte Nachricht — { type, payload } — kombiniert mit einem einzelnen Listener, der auf type verzweigt und jede Aktion auf den lokalen Zustand anwendet. Dadurch sprechen alle Kontexte dasselbe Protokoll; die API selbst weist Nachrichten keine Bedeutung zu, daher definiert man sie selbst.

const channel = new BroadcastChannel("cart-sync");
let cart = [];

channel.onmessage = ({ data }) => {
  if (data.type === "ADD_ITEM") cart.push(data.payload);
  else if (data.type === "CLEAR") cart = [];
  render();
};

function addItem(item) {
  cart.push(item);       // diesen Tab sofort aktualisieren
  render();
  channel.postMessage({ type: "ADD_ITEM", payload: item });
}

Beachten Sie, dass der Sender seinen eigenen Zustand direkt vor dem Broadcasting aktualisiert — da ein Tab seine eigenen Nachrichten nicht empfängt, wird die lokale Änderung inline angewendet, während der Broadcast an alle anderen weitergeleitet wird.

Auth, Warenkorb und Einstellungen über Tabs hinweg synchronisieren

Die wertvollsten Anwendungsfälle sind Session-Synchronisierung (ein Logout-Broadcast leert jeden Tab), Warenkorb-/Einstellungs-/Theme-Synchronisierung sowie Multi-Tab-Formular-Autofill. In React erstellt man den Kanal innerhalb von useEffect und gibt ein Cleanup zurück, das close() aufruft; lässt man dies weg, hinterlässt jedes Remount einen aktiven Listener-Leak.

import { useEffect } from "react";

function useAuthSync(onLogout) {
  useEffect(() => {
    const channel = new BroadcastChannel("auth");
    channel.onmessage = ({ data }) => {
      if (data.type === "LOGOUT") onLogout();
    };
    return () => channel.close();
  }, [onLogout]);
}

// beim Logout: lokale Session leeren, dann
// new BroadcastChannel("auth").postMessage({ type: "LOGOUT" })

In Svelte 5 (Runes) wird der Kanal in einem Store gekapselt. Ein wichtiger Hinweis: postMessage benötigt einen einfachen Clone, keinen reaktiven Proxy. Daher sollte der Wert vor dem Broadcasting durch $state.snapshot geleitet werden — ein Proxy würde den Structured-Clone-Schritt zum Scheitern bringen.

// theme.svelte.js — Svelte 5 (Runes)
export class ThemeStore {
  value = $state("light");
  #channel = new BroadcastChannel("theme");
  constructor() {
    this.#channel.onmessage = ({ data }) => { this.value = data; };
  }
  set(next) {
    this.value = next;
    this.#channel.postMessage($state.snapshot(next));
  }
}

Die Fallstricke, die die meisten Tutorials übersehen

Vier Verhaltensweisen führen zu Implementierungsproblemen, und genau dort werden die meisten Anleitungen vage:

  • Ein Tab kann seine eigenen Broadcasts nicht empfangen. Nachrichten werden an jeden lauschenden Kanal gesendet, außer an das Objekt, das die Nachricht gesendet hat — dies ist spezifikationskonformes Verhalten und durchaus nützlich: Es verhindert die endlosen Rückkopplungsschleifen, gegen die man sich andernfalls mit einer Self-ID-Prüfung absichern müsste. Die eigene Zustandsänderung des Senders wird inline angewendet.
  • „Same-Origin” bedeutet tatsächlich „gleiche Storage-Partition”. Storage wird nach Top-Level-Site partitioniert, sodass ein Cross-Site-iFrame keinen Kanal mit einer Top-Level-Seite teilt, selbst bei gleicher Origin. Kanäle überbrücken niemals verschiedene Subdomains.
  • Es ist eine Pipe, kein Speicher. BroadcastChannel transportiert Nachrichten, speichert jedoch nichts. Kombinieren Sie ihn mit localStorage oder IndexedDB — sowohl für Persistenz als auch zur Initialisierung von Tabs, die nach dem Auslösen eines Ereignisses geöffnet werden. Ein später geöffneter Tab, der den Broadcast nie empfangen hat, liest beim Mount den persistierten Zustand.
  • Immer close() beim Unmount aufrufen. Das Schließen trennt das Objekt vom Kanal und gibt es für die Garbage Collection frei; ein pro Komponente erstellter und nie geschlossener Kanal hinterlässt bei jedem Remount einen Listener-Leak.

Browser-Unterstützung und ein feature-erkannter Fallback

BroadcastChannel ist Baseline und seit März 2022 weitgehend verfügbar. Aktuelles Safari unterstützt es vollständig — die Behauptung „funktioniert nicht in WebKit” stammt aus der Zeit vor Safari 15.4. Laut caniuse und dem Chrome for Developers Blog sind die Mindestversionen Chrome 54+, Edge 79+, Firefox 38+, Safari 15.4+ (macOS und iOS), Opera 41+ und Samsung Internet 7.2+; nur der Internet Explorer hat es nie unterstützt.

Für Browser vor 2022 kann mit 'BroadcastChannel' in window eine Feature-Erkennung durchgeführt und auf ein storage-Event-Shim zurückgegriffen werden, das dieselbe Schnittstelle bereitstellt:

function createChannel(name) {
  if ("BroadcastChannel" in window) return new BroadcastChannel(name);
  // Fallback: localStorage "storage"-Event (nur Strings)
  return {
    postMessage: (data) =>
      localStorage.setItem(name, JSON.stringify({ data, t: Date.now() })),
    set onmessage(fn) {
      window.addEventListener("storage", (e) => {
        if (e.key === name && e.newValue) fn({ data: JSON.parse(e.newValue).data });
      });
    },
    close() {},
  };
}

Greifen Sie zuerst auf die native API zurück und betrachten Sie das Shim als Absicherung. In allen aktuellen Browsern ist BroadcastChannel bereits vorhanden — wählen Sie einen Kanalnamen, standardisieren Sie auf { type, payload }, persistieren Sie, was einen vollständigen Schließvorgang überstehen muss, und der veraltete Tab-Fehler in Ihrer Anwendung verschwindet.

FAQs

Funktioniert BroadcastChannel über verschiedene Subdomains hinweg?

Nein. BroadcastChannel ist auf dieselbe Storage-Partition beschränkt, nicht nur auf dieselbe Origin, und überbrückt niemals verschiedene Subdomains. Eine Seite auf app.example.com und eine Seite auf account.example.com können keinen Kanal teilen. Selbst bei gleicher Origin teilt ein Cross-Site-iFrame keinen Kanal mit der Top-Level-Seite, da Storage nach Top-Level-Site partitioniert wird. Für die subdomain-übergreifende Synchronisierung ist ein Server oder WebSocket erforderlich.

Was ist der Unterschied zwischen BroadcastChannel und dem storage-Event für die tab-übergreifende Synchronisierung?

BroadcastChannel transportiert Structured-Clone-Payloads wie Objekte, Maps, Sets und Blobs direkt, speichert jedoch nichts. Das localStorage-storage-Event überträgt nur Strings, was bei jeder Nachricht Serialisierung und Parsing erfordert, schreibt jedoch als Nebeneffekt dauerhaften Zustand. Verwenden Sie BroadcastChannel für reichhaltige, nachrichtengesteuerte Synchronisierung und kombinieren Sie ihn mit localStorage oder IndexedDB, wenn zusätzlich Persistenz benötigt wird oder Tabs initialisiert werden müssen, die nach dem Auslösen eines Ereignisses geöffnet werden.

Empfängt der Tab, der eine BroadcastChannel-Nachricht sendet, seine eigene Nachricht?

Nein. Laut Spezifikation wird eine Nachricht an jedes BroadcastChannel-Objekt gesendet, das auf den Kanal lauscht, außer an das Objekt, das sie gesendet hat. Ein Tab empfängt niemals seine eigenen Broadcasts, was die endlosen Rückkopplungsschleifen verhindert, gegen die man sich andernfalls mit einer Self-ID-Prüfung absichern müsste. Aus diesem Grund sollte die eigene Zustandsänderung des Senders inline angewendet werden, bevor postMessage aufgerufen wird, und der Broadcast wird dann an alle anderen Kontexte weitergeleitet.

Was passiert mit BroadcastChannel-Nachrichten, wenn alle Tabs geschlossen werden?

Sie gehen verloren. BroadcastChannel ist eine Pipe, kein Speicher: Er transportiert Nachrichten, speichert jedoch nichts. Daher ist jeder Zustand, der gebroadcastet wird, während kein anderer Tab lauscht, verloren, sobald alle Instanzen geschlossen werden. Ein nach einem Ereignis geöffneter Tab empfängt die ursprüngliche Nachricht nie. Um dies zu verhindern, sollte der Zustand in localStorage oder IndexedDB persistiert werden, und spät geöffnete Tabs sollten beim Mount aus diesem Speicher initialisiert werden, anstatt sich allein auf den Kanal zu verlassen.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.