Synchronisierung eines Warenkorbs über verschiedene Browser-Tabs: BroadcastChannel gegenüber localStorage
Warum die Storage-Events von localStorage identische Daten übertragen, warum Lösungen mit Date.now unzuverlässig sind und wie BroadcastChannel in Kombination mit einem dauerhaften Speicher das Problem der Warenkorb-Indikatoren zwischen Tabs löst.
Cross-Tab-Einkaufswagen-Badges scheinen wie eine fünfminütige Aufgabe, bis ein QA-Ticket beweist, dass die ursprüngliche Idee keine Nachrichten sendet. Die nachstehende Anleitung folgt einem gängigen Vorstellungsszenario: Tabs derselben Herkunft, kein gemeinsamer Speicher, kein Server-Ping – nur Tools der Browser-Plattform.
Szenario
Ein Käufer hat zwei Produkt-Tabellen geöffnet. Er fügt ein Artikel in Tab A hinzu. Das Header-Badge von Tab B sollte dies mitverfolgen. Andernfalls zeigt Tab B weiterhin die alte Anzahl an, und der Käufer geht davon aus, dass die Hinzufügung fehlgeschlagen ist.
Einschränkungen seitens des Interviewers: Die Tabellen dürfen keine JavaScript-Heaps teilen, und ein Netzwerk-Ausgleichsversuch für das Signal selbst ist nicht zulässig. Die Plattform muss die Benachrichtigung übertragen.
Erster Versuch: Speicherereignisse
Die meisten Kandidaten greifen auf localStorage zusammen mit dem storage-Listener zurück. Eine Schreiboperation in einem Tab informiert die anderen Tabs derselben Herkunft.
// Tab that adds the item
function addToCart(sku) {
cart.add(sku);
renderBadge();
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1
}));
}
// Every other tab
addEventListener('storage', (e) => {
if (e.key !== 'cart-sync') return;
const msg = JSON.parse(e.newValue);
if (msg.type === 'CART_ADD') { cart.add(msg.sku); renderBadge(); }
});
Dieser Entwurf funktioniert oft einwandfrei. Dann kommt die Anfrage.
Der QA-Bericht, der alles kaputt macht
Reproduzierung: Fügen Sie den dieselben SKU zweimal hinzu. Tab A zeigt die Menge 2 an. Tab B bleibt bei 1. Beim Neuladen von Tab B wird schließlich 2 angezeigt.
Es gibt keine Fehlermeldungen. Die Protokolle sind leer. Der Listener scheint in Ordnung zu sein – warum hat also der Peer die Aktualisierung übersehen?
Der Hinweis liegt darin, dass ein Neuladen das Problem behebt: Der Zustand war korrekt; nur das Live-Signal funktionierte nicht.
Ursache: Gleichwertige Werte werden ignoriert
Der HTML-Algorithmus setItem bricht ab, wenn der eintreffende String mit dem bereits gespeicherten Wert für diese Schlüssel übereinstimmt – aus der Spezifikation paraphrasiert als „Wenn der vorherige Wert dem neuen Wert entspricht, hört man auf.“ Es erfolgt keine Datenspeicherung auf der Festplatte, keine Übertragung und kein Wecken des Peers.
Identische Aktionen im Warenkorb können zu denselben Bytes serialisiert werden:
{"type":"CART_ADD","sku":"SKU-1029","qty":1}
Tab A führt weiterhin lokale Schreibvorgänge durch, nachdem es seinen eigenen add-Aufruf ausgeführt hat. Tab B erhält niemals ein storage-Event. Nach einem Neuladen liest Tab B den Speicher erneut und scheint in Ordnung zu sein – ein klassisches Problem bei intermittierenden Tests.
Warum Duplikate selten erscheinen, aber es nicht sind
Sequenzen, die abwechselnd Werte verwenden (A, dann B, dann wieder A), funktionieren weiterhin einwandfrei. Probleme treten bei identischen, aufeinanderfolgenden Datenpaketen auf:
- Doppelklicks, die denselben SKU hinzufügen
- Heartbeats, die den unveränderten Status „ONLINE“ erneut senden
- Mehrfache Meldungen zu
SESSION_EXPIRED, während eine langsame Tab-Seite noch lädt
Genau in diesen Momenten benötigen die betroffenen Systeme am dringendsten eine Warnung.
Die „Einzigartigkeit“ von Zeitstempeln ist anfällig
Ein gängiger Patch fügt Date.now() in das JSON ein, sodass die Zeichenketten unterschiedlich werden:
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1,
t: Date.now() // force the value to differ
}));
Millisekunden-Uhren kollidieren, wenn zwei Schreibvorgänge zur gleichen Millisekunde erfolgen. Ob das Patch in solchen Fällen funktioniert, hängt vom Zufall des Schedulers ab – manchmal ja, manchmal nein. Korrektheit, die von der Granularität einer normalen Uhr abhängt, ist keine echte Korrektheit. Verwenden Sie lieber crypto.randomUUID() (oder einen anderen stark eindeutigen Token), wenn Sie gezwungen sind, im Speicher zu bleiben.
Aufräumen führt zu doppeltem Versand
Einzigartige Payloads dauerhaft beizubehalten, ist unübersichtlich, deshalb rufen viele sofort nach setItem auch removeItem auf. Dadurch werden zwei Benachrichtigungen ausgesendet: eine für das Schreiben und eine für das Löschen.
event 1 → { key:'cart-sync', oldValue: null, newValue: '{"type":"CART_ADD",…}' }
event 2 → { key:'cart-sync', oldValue: payload, newValue: null }
Sofern der Handler newValue === null ignoriert, wendet der Peer die Änderung am Warenkorb zweimal an. Das Design mit Speicher als Nachrichtenbus erfordert nun Eindeutigkeit, Null-Prüfungen, Parsing sowie Aufräumarbeiten – schließlich handelt es sich bei der API um einen Schlüssel/Wert-Speicher, der gelegentlich Signale sendet, und nicht um eine Nachrichtenwarteschlange.
Empfohlenes Werkzeug: BroadcastChannel
Wenn es um das Versenden von Nachrichten geht – und nicht um die Persistenz – sollte die Messaging-API verwendet werden:
const bus = new BroadcastChannel('cart-sync');
// send — the same message, as many times as you like
bus.postMessage({ type: 'CART_ADD', sku, qty: 1 });// receive
bus.onmessage = (e) => {
if (e.data.type === 'CART_ADD') { cart.add(e.data.sku); renderBadge(); }
};addEventListener('pagehide', () => bus.close());
Mehrfache, identische Objekte werden dennoch übermittelt. Es findet kein Kurzschluss bei Gleichheitsprüfungen statt. Ein strukturiertes Klonen ermöglicht die Verwendung reicherer Datentypen als JSON (Date, Map, Set, typisierte Arrays). Betrachten Sie den Speicher als eine Datenbank mit optionalen Änderungsmitteilungen und BroadcastChannel als eine Mitteilung ohne Datenbank.
Weitere wichtige Fragen in der Produktion
Selbstübermittlung. Das Channel-Objekt erhält keine eigene Nachricht, doch eine andere Instanz desselben Namens im selben Dokument sowie Iframes derselben Herkunft schon – es sei denn, es gibt mehrere Abonnenten auf derselben Seite. Entfernen Sie Duplikate, falls dies der Fall ist.
Synchron vs. asynchron. Die Übertragung wird im Ereigniszyklus des Empfängers in der Warteschlange abgelegt (asynchron). Das Klonen während von postMessage ist synchron und lehnt sofort nicht-klonierbare Werte ab (DataCloneError für Funktionen).
Spät geöffnete Tabellen. Kanäle wiedergeben die Historie nicht. Eine nach dem Hinzufügen geöffnete Tabellenseite zeigt weiterhin das alte Abzeichen, es sei denn, sie liest aus einem dauerhaften Speicher. Verwendetes Muster:
// The shape that actually ships:
// durable store = the truth, channel = the doorbell
async function addToCart(sku) {
await idbPut('cart', sku); // atomic, survives reloads
bus.postMessage({ type: 'CART_CHANGED' }); // just a signal
}
bus.onmessage = async () => renderBadge(await idbCount('cart'));
Dauerhafte Daten gelten als wahr; der Kanal ist lediglich die Glocke, die ankündigt, dass sich diese Wahrheit geändert hat.
Zähler im Speicher. Lesen/Ändern/Schreiben zwischen Tabellen führt zum Verlust von Aktualisierungen; die Plattform bietet keinen Schutzmechanismus. Erfinden Sie keinen verteilten Zähler in localStorage.
Wo Speicherlösungen weiterhin überlegen sind. Einstellungen wie Theme oder Lokalisation, die synchron vor dem Zeichnen gelesen werden müssen und selten ändern, sollten mit storage-Events für Werte und BroadcastChannel für Ereignisse gehandhabt werden.
Selbstprüfung
Mit dem naiven Ansatz des gleichwertigen Speichers ergeben zwei identische Einträge, die einen Sekundenabstand haben, wie viele Peer-Ereignisse?
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
// … one second later, same product added again …
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
Antwort: Ein einziges Ereignis. Die verstrichene Zeit ist unerheblich; nur eine geänderte Zeichenkette löst eine Benachrichtigung aus.
Zusammenfassung des Interviews
Nutzen Sie BroadcastChannel für Signale zwischen Tabs, behalten Sie IndexedDB oder ähnliche Systeme als primäre Quelle für den Warenkorb bei und verweisen Sie auf die sofortige Rückgabe bei gleichen Werten, falls ein Speicher als Kommunikationskanal vorgeschlagen wird. Erwähnen Sie außerdem die Limits für die Wiedergabe in später geöffneten Tabs, das Mehrkanal-Selbstecho sowie die Gründe, warum lock-freie Zähler in localStorage versagen.
Auszüge
Die Synchronisierung der Cross-Tab-Oberfläche ist ein Kommunikationsproblem, das in einem Speicherkostüm verpackt ist. Behandeln Sie den dauerhaften Zustand sowie Benachrichtigungen als getrennte Schichten, wählen Sie APIs aus, die zu jeder Schicht passen, und testen Sie identische Abfolgen von Aktionen – die Schulungshandbücher überspringen diese Aspekte, während Produktivnutzer auf Doppelklicks angewiesen sind.
Zusätzliche Hinweise für die Produktion
Mobile Browser können Hintergrundtabellen aggressiv löschen; ein Badge-Update bei visibilitychange, das den dauerhaften Speicher erneut liest, deckt Fälle ab, in denen eine Nachricht während des Einfrierens übersehen wurde. Kombinieren Sie dies mit dem BroadcastChannel für Vordergrundkommunikation.
Automatisierte Tests sollten zwei Kontexte (Playwright-Tabellen) verwenden und sowohl die gleichbleibende Speicherung der Daten als auch die Übermittlung von Duplikaten über BroadcastChannel überprüfen. Dokumentieren Sie das gewählte Schema für die dauerhaften Schlüssel, damit eine zukünftige Server-Synchronisierung ohne Erfindung einer zweiten Quelle der Wahrheit erfolgen kann.
Feature-Flags können manchmal das Verhalten der „Live-Badge“-Funktion steuern. Die dauerhafte Schreiboperation muss bedingungslos erfolgen; nur die Glockenfunktion kann optional sein. Andernfalls weicht ein Tab mit deaktiviertem Flag dauerhaft von den Tabs ab, bei denen das Flag aktiv ist.
Was die Sicherheit betrifft, so sollten Authentifizierungstoken niemals in localStorage-Nachrichten gespeichert werden. Produktkennungen im Warenkorb sind in Ordnung; Sitzungsgeheimnisse hingegen nicht. Es ist besser, unsichtbare IDs zu senden und jedem Tab zu erlauben, nach einer validierten Sitzungsprüfung privilegierte Details über httpOnly-Kanäle oder aus dem Speicher abzurufen.
Falls die Online-Shop-Plattform mehrere Subdomains umfasst, kann BroadcastChannel diese nicht überschreiten. Möglichkeiten sind ein gemeinsamer Shared Worker auf einem gemeinsamen Elterndomänennamen oder serverseitig gesendete Ereignisse, die nach dem Warenkorb-ID gekennzeichnet sind. Diese Einschränkung sollte bereits früh in den Design-Überprüfungen angesprochen werden.
Die Internationalisierung des Badges (Regeln für die Mehrzahlform) sollte in jedem Tab nach dem Abrufen der Anzahl durchgeführt werden – senden Sie vorformatierte Zeichenfolgen nur dann, wenn garantiert ist, dass alle Lokalisierungen identisch sind.
Zum Schluss: Messen Sie, wie oft Doppeladdierungen von SKU-Werten in den Analysedaten auftreten. Wenn diese Häufigkeit erheblich ist, hätte die Falle des gleichwertigen Speicherns zu einem latenten Produktionsproblem geführt, das auf den ersten leistungsstarken Mehrfachtab-Nutzer wartet.
Übertragung des Interviews in ein Design-Dokument
Wenn Sie dies für das Team dokumentieren, trennen Sie drei Entscheidungen voneinander: (1) Welcher dauerhafte Speicher die Warenkorbdaten enthält, (2) Welcher Transportmechanismus die Peer-Tabes weckt und (3) Wie der Badge-Reduzierer die Benachrichtigungen interpretiert. Wenn man diese Entscheidungen auf „einfach localStorage verwenden“ reduziert, entsteht die Gleichheitsfalle.
Ein kurzes Design-Dokument kann ein Sequenzdiagramm enthalten: Benutzerklick → Änderung in IndexedDB → postMessage über BroadcastChannel → Die Peer-Tabes ungültigen Badge-Anfragen senden. Beachten Sie auch mögliche Fehlerfälle: Nicht unterstützte Kanäle (selten in modernen Browsern, aber überprüfen), Besonderheiten im Privatmodus sowie Browser mit mehreren Profilen, die den Speicher isolieren.
Test-Checkliste
- Doppelte Hinzufügung desselben SKU mit Klingel nur für Speicherung (Erwartet: Fehlschlag)
- Doppelte Hinzufügung mit BroadcastChannel (Erwartet: Zwei Aktualisierungen)
- Öffnen eines dritten Tabs nach der Hinzufügung (Erwartet: Korrekte Anzahl aus dauerhafter Leseoperation, nicht aus Wiederholung)
- Schnelle Hinzufügungen innerhalb einer Millisekunde mit eindeutigen Zeitstempeln (Erwartet: gelegentliche Fehlschläge)
- setItem + removeItem ohne Null-Prüfung (Erwartet: Doppelte Zählung)
Die Automatisierung dieser Checkliste in CI verhindert Rückfälle, wenn jemand wieder auf Speicherereignisse zurückgreift.
Warum Interviewer diese Aufgabe mögen
Dies belohnt das Lesen der Spezifikationen, nicht das Auswendiglernen von API-Names. Kandidaten, die nur kurz in MDN geschaut haben, übersehen die Rückgabe von Gleichheitswerten. Kandidaten, die bereits mehrfach Tab-basierte Benutzeroberflächen implementiert haben, nennen BroadcastChannel und dauerhafte Speicher ohne Anleitung. Die Nachfragen zu späten Tabs und lockfreien Zählern zeigen, ob die Antwort aus einem Blogbeitrag stammt oder auf eigener Erfahrung beruht.
Für die mit nach Hause zu nehmenden Varianten sollte man ein kleines Demo-Repository mit zwei Routen sowie eine README-Datei anfordern, die die gewählten Schichten beschreibt. Die Prüfer sollten zwei Fenster öffnen und klicken – praktische Demonstrationen sind besser als ein Absatz Theorie.
Verwandte Muster
Präsenzindikatoren, kollaborative Cursor (leichtgewichtig) sowie das Abmelden nutzen dasselbe Doorbell-Modell. Die gemeinsame Bearbeitung von Dokumenten erfordert in der Regel CRDTs oder einen Server; nutzen Sie BroadcastChannel nicht als Konsistenzprotokoll. Behalten Sie das Einkaufswagen-Beispiel einfach: eventuelle Synchronisierung der Statusindikatoren, ein zuverlässiger, dauerhafter Einkaufswagen sowie eine optional spätere Abstimmung mit dem Server.
Lassen Sie die Funktionsweise in der Produktion einfach und unkompliziert bleiben: ein einziger, dauerhafter Einkaufswagen, ein einziger Doorbell, explizite Tests auf doppelte Aktionen – und keine cleveren Lösungen, die auf Millisekundenuhren beruhen. Genau diese einfache Struktur sorgt dafür, dass die Statusindikatoren in mehreren Tabs weiterhin zuverlässig funktionieren, selbst wenn die Nutzer mehr Fenster öffnen als in der ursprünglichen Demo.
Diese Kombination reicht aus. Fertig.