Offline-Aktionen automatisch nachholen
Ein Formular wird abgeschickt, aber die Verbindung ist gerade weg. Statt eine Fehlermeldung zu zeigen und die Eingabe zu verwerfen, merkt sich die Background Sync API die Aktion und liefert sie zuverlaessig nach, sobald das Netz zurueck ist, auch wenn der Tab laengst geschlossen wurde.
Inhaltsverzeichnis
- 1. Das Problem: Offline-Aktionen gehen sonst einfach verloren
- 2. Einen Sync-Task registrieren
- 3. Die Outbox: Aktionen in IndexedDB zwischenspeichern
- 4. Das sync-Event im Service Worker behandeln
- 5. Browser-Unterstuetzung und Feature Detection
- 6. Abgrenzung zu Periodic Background Sync
- 7. Konfliktbehandlung: was bei zeitversetztem Versand passieren kann
- 8. Nutzer-Feedback: Transparenz ueber ausstehende Aktionen
- 9. Fazit: Zuverlaessigkeit statt Fehlermeldung
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem: Offline-Aktionen gehen sonst einfach verloren
Ohne Background Sync haben Web-Apps genau zwei schlechte Optionen, wenn ein Nutzer offline eine Aktion ausloest: entweder die Anfrage sofort scheitern lassen und eine Fehlermeldung zeigen, oder still versuchen, per fetch() zu senden, und hoffen, dass die Promise irgendwann doch aufgeloest wird. Beides fuehrt zu Frust, insbesondere bei mobilen Nutzern mit instabiler Verbindung in Aufzuegen, Tunneln oder auf dem Land.
Die Background Sync API loest das strukturell: Statt die Anfrage direkt zu senden, wird die Absicht registriert, etwa 'sende dieses Formular ab', und der Browser uebernimmt die Verantwortung, diese Absicht zuzustellen, sobald wieder eine Verbindung besteht, selbst wenn der Nutzer die Seite laengst verlassen hat. Der Service Worker wird dafuer vom Betriebssystem geweckt, unabhaengig vom Lebenszyklus des Tabs.
2. Einen Sync-Task registrieren
Der Ablauf beginnt im Hauptthread: Statt fetch() direkt aufzurufen, wird die Aktion zunaechst lokal gespeichert, meist in IndexedDB, und anschliessend ein Sync-Tag ueber registration.sync.register() beim Service Worker angemeldet. Dieses Tag ist ein frei waehlbarer String, der spaeter im Service Worker identifiziert, welche Art von Aktion nachgeholt werden soll.
Wichtig ist, dass register() selbst keine Garantie fuer sofortige Ausfuehrung gibt. Ist die Verbindung bereits vorhanden, feuert das sync-Event meist innerhalb weniger Sekunden, ist sie es nicht, wartet der Browser, bis das Betriebssystem eine stabile Verbindung meldet, und kann dabei durchaus Batterie- und Netzwerk-Heuristiken einbeziehen, die Entwickler nicht direkt beeinflussen koennen.
// app.js: Formular offline-sicher abschicken
async function submitComment(formData) {
await saveToOutbox(formData); // IndexedDB, siehe naechster Abschnitt
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
try {
await registration.sync.register('sync-comments');
} catch (err) {
// Background Sync nicht verfuegbar -> sofortiger Fallback-Versuch
await trySendDirectly(formData);
}
} else {
await trySendDirectly(formData);
}
}
3. Die Outbox: Aktionen in IndexedDB zwischenspeichern
Background Sync transportiert selbst keine Daten, es liefert lediglich ein Signal 'jetzt synchronisieren'. Die eigentlichen Nutzdaten, also etwa der Formularinhalt oder die Like-Aktion, muessen deshalb vorher in einem persistenten Speicher abgelegt werden, der sowohl vom Hauptthread als auch vom Service Worker erreichbar ist. IndexedDB ist dafuer die naheliegende Wahl, da localStorage im Service-Worker-Kontext nicht verfuegbar ist.
Ueblich ist ein Outbox-Muster: Ein Object Store haelt ausstehende Aktionen mit Status 'pending', der Service Worker liest beim sync-Event alle offenen Eintraege, versucht sie zu senden und markiert erfolgreiche Eintraege als erledigt oder loescht sie direkt. So bleibt der Zustand konsistent, selbst wenn zwischenzeitlich mehrere Sync-Versuche noetig sind.
// outbox.js: einfache IndexedDB-Outbox
const DB_NAME = 'outbox-db';
const STORE = 'pending-comments';
function openOutbox() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => {
req.result.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveToOutbox(data) {
const db = await openOutbox();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readwrite');
tx.objectStore(STORE).add({ ...data, createdAt: Date.now() });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
4. Das sync-Event im Service Worker behandeln
Feuert das sync-Event, liegt event.tag mit dem registrierten Tag vor, sodass ein Service Worker mehrere unterschiedliche Sync-Aufgaben unterscheiden kann, etwa 'sync-comments' fuer Kommentare und 'sync-likes' fuer Like-Klicks. Der Handler liest alle offenen Eintraege aus der Outbox, sendet sie sequenziell oder parallel an den Server und entfernt erfolgreich verarbeitete Eintraege.
Genau wie beim push-Event muss auch hier event.waitUntil() verwendet werden, damit der Service Worker nicht vorzeitig terminiert wird, bevor alle ausstehenden Anfragen abgeschlossen sind. Schlaegt der Versand erneut fehl, etwa weil die Verbindung sofort wieder abbricht, sollte die Promise mit einem Fehler abgelehnt werden, denn dann registriert der Browser automatisch einen erneuten Versuch mit exponentiellem Backoff.
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-comments') {
event.waitUntil(flushOutbox());
}
});
async function flushOutbox() {
const pending = await getAllFromOutbox();
for (const item of pending) {
try {
const res = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
});
if (!res.ok) throw new Error('Server lehnte Kommentar ab');
await removeFromOutbox(item.id);
} catch (err) {
// Nicht aus der Outbox entfernen -> naechster sync-Versuch greift erneut
throw err;
}
}
}
5. Browser-Unterstuetzung und Feature Detection
Die Background Sync API ist bislang auf Chromium-basierte Browser beschraenkt, Safari und Firefox unterstuetzen sie nicht. Das macht ein sauberes Feature Detection zwingend: Vor jedem register()-Aufruf muss geprueft werden, ob sowohl 'serviceWorker' in navigator als auch 'SyncManager' in window vorhanden sind, sonst wirft der Aufruf eine Exception.
Fuer nicht unterstuetzende Browser braucht es einen Fallback, der die Aktion trotzdem irgendwann nachholt, etwa ein eigener online-Event-Listener, der bei Rueckkehr der Verbindung manuell versucht, ausstehende Outbox-Eintraege zu senden. So bleibt die Grundfunktion auch ohne native API erhalten, wenn auch ohne die Zuverlaessigkeit der Betriebssystem-Integration.
// Fallback fuer Browser ohne Background Sync
function registerFallbackSync() {
window.addEventListener('online', async () => {
const pending = await getAllFromOutbox();
for (const item of pending) {
await trySendDirectly(item);
}
});
}
const supportsBackgroundSync =
'serviceWorker' in navigator && 'SyncManager' in window;
if (!supportsBackgroundSync) {
registerFallbackSync();
}
6. Abgrenzung zu Periodic Background Sync
Neben der regulaeren Background Sync API existiert die verwandte, aber deutlich restriktivere Periodic Background Sync API, die periodische Aktualisierungen im Hintergrund erlaubt, etwa das Nachladen neuer Inhalte alle paar Stunden. Sie loest ein anderes Problem: nicht das Nachholen einer konkreten Nutzeraktion, sondern das proaktive Aktualisieren von Daten, ohne dass der Nutzer die App geoeffnet hat.
Periodic Background Sync verlangt zusaetzlich, dass die Web-App installiert wurde und eine gewisse 'Site Engagement Score' erreicht, was sie fuer die meisten Projekte deutlich unzuverlaessiger macht als die regulaere Background Sync API. Fuer das Nachholen konkreter Nutzeraktionen wie Formular-Absenden bleibt die einmalige, ereignisgetriebene Background Sync API die richtige Wahl.
7. Konfliktbehandlung: was bei zeitversetztem Versand passieren kann
Wird eine Aktion erst Stunden nach der eigentlichen Nutzereingabe nachgeholt, koennen sich zwischenzeitlich die Rahmenbedingungen geaendert haben: ein Like-Ziel existiert nicht mehr, ein Warenkorb-Preis hat sich geaendert, oder der Nutzer hat sich zwischenzeitlich ausgeloggt. Der Server-Endpunkt fuer nachgeholte Aktionen sollte deshalb defensiv programmiert sein und klare Fehlercodes fuer solche Faelle liefern.
Auf Client-Seite empfiehlt sich, jeden Outbox-Eintrag mit einem Zeitstempel zu versehen und bei der Verarbeitung im Service Worker zu pruefen, ob die Aktion ueberhaupt noch sinnvoll ist, etwa durch ein Ablaufdatum. Ein vor drei Tagen offline gesetzter Like sollte im Zweifel eher verworfen als stillschweigend Stunden nach dem eigentlichen Klick zugestellt werden, wenn der Kontext fuer den Nutzer laengst nicht mehr relevant ist.
8. Nutzer-Feedback: Transparenz ueber ausstehende Aktionen
Aus Nutzersicht ist es wichtig, sichtbar zu machen, dass eine Aktion zwar angenommen, aber noch nicht endgueltig zugestellt wurde. Ein einfaches UI-Signal, etwa ein kleines Wolken-Icon mit 'wird gesendet, sobald wieder Netz da ist' neben dem Kommentar, verhindert Verwirrung und wiederholtes Absenden derselben Aktion aus Ungeduld.
Der Service Worker kann per postMessage() an offene Tabs melden, wenn ein Outbox-Eintrag erfolgreich verarbeitet wurde, sodass die UI in Echtzeit aktualisiert werden kann, falls der Nutzer die Seite noch offen hat. Ist die Seite geschlossen, uebernimmt beim naechsten Besuch ein Abgleich zwischen lokalem und Server-Zustand die Aktualisierung der Anzeige.
// sw.js: Tabs ueber erfolgreichen Sync informieren
async function notifyClients(message) {
const allClients = await clients.matchAll({ type: 'window' });
for (const client of allClients) {
client.postMessage(message);
}
}
// nach erfolgreichem Versand innerhalb von flushOutbox():
await notifyClients({ type: 'sync-success', id: item.id });
9. Fazit: Zuverlaessigkeit statt Fehlermeldung
Die Background Sync API verschiebt die Verantwortung fuer eine zuverlaessige Zustellung vom Nutzer weg hin zum Browser: Statt eine fehlgeschlagene Anfrage manuell erneut anzustossen, merkt sich die App die Absicht und der Browser garantiert die Zustellung, sobald es die Netzwerksituation zulaesst. Kombiniert mit einer IndexedDB-Outbox entsteht so ein robustes Offline-First-Verhalten ohne komplexe eigene Retry-Logik.
Wegen der eingeschraenkten Browser-Unterstuetzung bleibt ein Fallback fuer Safari und Firefox unverzichtbar, und Entwickler sollten Nutzern stets transparent machen, dass eine Aktion noch aussteht. Die folgende Tabelle stellt die zentralen Bausteine im Ueberblick dar.
| Baustein | Ort | Aufgabe | Wichtiges Detail |
|---|---|---|---|
| Outbox (IndexedDB) | Client + Service Worker | Nutzdaten persistent zwischenspeichern | localStorage funktioniert im Service Worker nicht |
| sync.register() | Client | Sync-Task anmelden | Keine Garantie fuer sofortige Ausfuehrung |
| sync-Event | Service Worker | Ausstehende Eintraege verarbeiten | event.waitUntil() zwingend erforderlich |
| Fehlgeschlagener Versand | Service Worker | Automatischer Retry | Promise ablehnen loest erneuten Versuch aus |
Mironsoft
Moderne Browser-APIs, Performance und wartbares JavaScript
JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?
Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.
Code-Review
Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.
Performance-Optimierung
Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.
Modernisierung
Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.
10. Zusammenfassung
Background Sync API: Das Wichtigste auf einen Blick
Signal statt Daten
Background Sync liefert nur das 'jetzt synchronisieren'-Signal, Daten liegen in IndexedDB
Registrierung
registration.sync.register() meldet einen Sync-Task mit frei waehlbarem Tag an
Verarbeitung
Service Worker liest die Outbox im sync-Event und sendet ausstehende Eintraege nach
Support
Nur Chromium-Browser, Fallback ueber online-Event fuer Safari und Firefox noetig