EMZETT.
Login

Offline-First und Background Sync (PWA)

Kurz: Nutzeraktionen (z. B. “In den Warenkorb legen”) werden bei fehlender Internetverbindung nicht verworfen, sondern lokal im Browser zwischengespeichert und automatisch nachgeholt, sobald die Verbindung wieder da ist.

Genauer: IndexedDB (statt localStorage) wird genutzt, weil ein Service Worker im Hintergrund darauf zugreifen können muss, auch wenn die eigentliche Seite gar nicht geöffnet ist — localStorage ist aus dem Service-Worker-Kontext heraus nicht erreichbar. Die Background Sync API (nur Chrome/Edge/Android) erlaubt dem Service Worker, das Nachholen auch anzustoßen, wenn die Seite geschlossen ist; als universeller Fallback (funktioniert auch auf Safari/iOS) dient zusätzlich ein einfacher 'online'-Browser-Event-Listener.

Kontext bei uns: app/utils/offlineQueue.ts bei Emzett (CART_QUEUE_STORE) für den Warenkorb — mit idempotentem Sync über /api/cart/sync, damit ein doppelter Sync-Versuch (Background Sync UND 'online'-Event) keine doppelten Einträge erzeugt.

Im Detail

Idempotenz als Kernanforderung

“Idempotent” ist hier das entscheidende Stichwort: Da sowohl die Background Sync API als auch der 'online'-Event-Fallback denselben Sync auslösen können (und theoretisch sogar beide gleichzeitig feuern könnten, wenn der Browser genau in dem Moment online geht), muss der Server so gebaut sein, dass ein zweimaliges Senden derselben Aktion KEINEN doppelten Effekt hat — typischerweise über eine eindeutige Client-generierte ID pro Aktion (meist eine UUID, lokal beim Erstellen der Aktion generiert), die der Server beim zweiten Eintreffen erkennt und ignoriert, statt den Artikel ein zweites Mal in den Warenkorb zu legen. Diese ID muss VOR dem eigentlichen Sync generiert werden (nicht erst beim Senden), damit sie bei jedem Sync-Versuch identisch bleibt — würde man bei jedem Versuch eine neue ID erzeugen, wäre die Idempotenz-Prüfung wirkungslos.

Typischer Ablauf

Nutzer klickt “In den Warenkorb” ohne Verbindung → Aktion landet sofort in der IndexedDB-Queue UND wird optimistisch in der UI angezeigt, als wäre sie schon durch → sobald navigator.onLine wieder true wird (oder der Service Worker über Background Sync geweckt wird), verarbeitet ein Sync-Handler die Queue der Reihe nach und schickt jede Aktion an den Server → bei Erfolg wird der Queue-Eintrag gelöscht, bei Fehler bleibt er für einen späteren Versuch erhalten. Die optimistische UI-Aktualisierung ist dabei bewusst gewählt: Ein Nutzer, der offline einkauft, soll nicht das Gefühl haben, die App sei “kaputt” — stattdessen wirkt die Aktion sofort erfolgreich, mit einem dezenten Hinweis (“wird synchronisiert, sobald wieder online”), falls tatsächlich etwas fehlschlägt.

self.addEventListener("sync", (event) => {
  if (event.tag === "cart-sync") {
    event.waitUntil(processQueuedCartActions());
  }
});

Warum IndexedDB statt localStorage

localStorage ist synchron und blockiert dabei kurzzeitig den Haupt-Thread, was bei größeren Datenmengen zu spürbaren Rucklern führen kann; IndexedDB ist dagegen asynchron und für strukturierte, größere Datenmengen ausgelegt (Objektspeicher mit Indizes, Transaktionen). Der entscheidende Punkt hier ist aber ein anderer: localStorage ist an den Hauptthread des Dokuments gebunden und aus einem Service-Worker-Kontext heraus schlicht nicht erreichbar — ein Service Worker läuft in einem eigenen, vom Dokument unabhängigen Thread, genau damit er auch arbeiten kann, wenn die Seite selbst gar nicht geöffnet ist. IndexedDB ist eine der wenigen Storage-APIs, die aus beiden Kontexten (Dokument UND Service Worker) gleichermaßen zugreifbar ist.

Grenzen der Background Sync API

Die Background Sync API ist bewusst kein Garant für sofortige Ausführung — der Browser entscheidet selbst, wann er den Sync tatsächlich anstößt (abhängig von Netzwerkqualität, Akkustand, etc.), typischerweise aber innerhalb weniger Sekunden nach Wiederverbindung. Browserunterstützung ist zudem uneinheitlich: Safari (und damit alle Browser auf iOS, da Apple dort eine eigene WebKit-Engine erzwingt) unterstützt die API bis heute nicht — deshalb ist der 'online'-Event-Fallback kein optionales Extra, sondern für plattformübergreifende Zuverlässigkeit zwingend notwendig.

Siehe auch: proxy.ts, Verbindungsloses Protokoll