12k
All articles

Offline-Formularübermittlung mit Background Sync

Formularübermittlungen in IndexedDB zwischenspeichern, mit Background Sync und Online-Fallbacks erneut senden und doppelte Bestellungen per Idempotency-Key verhindern.

OpenReplay Team
OpenReplay Team
Offline-Formularübermittlung mit Background Sync

Um eine Formularübermittlung auch bei einer unterbrochenen oder instabilen Verbindung sicherzustellen, schreiben Sie die Übermittlung zunächst in IndexedDB, bevor Sie die Netzwerkverbindung nutzen, registrieren Sie einen Background-Sync-Tag und lassen Sie den Service Worker die eingereihte Anfrage aus seinem sync-Event wiederholen, sobald die Verbindung wiederhergestellt ist — fügen Sie anschließend einen online-Event-Fallback für Browser hinzu, die Background Sync nicht unterstützen. Dieser letzte Schritt ist nicht optional: Die Background-Sync-API ist ausschließlich in Chromium-basierten Browsern verfügbar, weshalb die Kombination aus IndexedDB-Warteschlange und online-Event-Listener die universelle Basislösung darstellt, auf der Background Sync als Erweiterung aufbaut.

Dieser Artikel führt anhand eines konkreten Beispiels — eines Kontakt- bzw. Bestellformulars — durch das vollständige Muster: dauerhafte Warteschlange, Sync-Registrierung, Service-Worker-Wiederholung, der universelle Fallback, Vermeidung doppelter Übermittlungen sowie das Bestätigungsproblem, vor dem kaum jemand warnt. Es wird vorausgesetzt, dass Sie bereits einen Service Worker registriert haben und mit Promises und fetch vertraut sind. Allgemeines Caching (Cache-First, Network-First) ist eine Voraussetzung, nicht Gegenstand dieses Artikels — dazu empfehlen sich MDNs Caching-Strategien und die Workbox-Strategiemodule.

Wichtige Erkenntnisse

  • Schreiben Sie die Übermittlung mit einer clientseitig generierten Request-ID, einem queuedAt-Zeitstempel und einem Wiederholungszähler vor dem Netzwerkaufruf in IndexedDB, damit ein unterbrochener POST bereits gesichert ist, statt verloren zu gehen.
  • One-off Background Sync (SyncManager) wird ausschließlich in Chromium-basierten Browsern unterstützt — Chrome, Edge, Opera und Samsung Internet — und fehlt in Firefox, Safari und iOS, weshalb ein online-Event-Fallback im Jahr 2026 zwingend erforderlich ist.
  • Im sync-Handler des Service Workers senden Sie jeden eingereihten Eintrag per POST, löschen ihn bei Erfolg und werfen bei einem Fehler eine Exception — das Werfen signalisiert dem Browser, die Registrierung beizubehalten und mit browserseitig verwaltetem exponentiellen Backoff erneut zu versuchen.
  • Senden Sie die Request-ID als Idempotency-Key-Header, damit eine Übermittlung, die der Server bereits akzeptiert hat, bei erneuter Zustellung dedupliziert wird und keine doppelten Bestellungen entstehen.
  • Das BackgroundSyncPlugin von Workbox übernimmt das Einreihen und Wiederholen für Sie, wiederholt jedoch ausschließlich echte Netzwerkfehler — eine 4xx- oder 5xx-Antwort wird als zugestellt gewertet und nicht erneut versucht.

Warum ein einfaches fetch beim Absenden offline versagt

Ein einfaches fetch beim Absenden eines Formulars schlägt still fehl, wenn das Gerät offline ist: Das Promise wird abgelehnt, die Anfrage erreicht den Server nie, und sofern keine explizite Fehlerbehandlung implementiert wurde, erhält der Nutzer keinerlei Rückmeldung. Service Worker lösen das Caching von Assets, wiederholen jedoch nicht automatisch die neuen Datenanfragen, die ein Formular stellt — ein fehlgeschlagener POST ist schlicht verloren.

Ein still fehlgeschlagener Offline-POST ist der klassische „Nutzer glaubt, erfolgreich abgeschickt zu haben; die Daten sind nie angekommen”-Fehler. Er ist zudem für das Backend-Monitoring unsichtbar, da die Anfrage das Backend nie erreicht hat — es gibt nichts zu protokollieren. Session Replay ist die Technik, die diese Sichtbarkeitslücke schließt: Ein Replay rekonstruiert die clientseitige Realität — das Tippen auf „Absenden”, den optimistischen „Danke!”-Bildschirm, die anschließende Navigation — die Sie dann damit abgleichen können, ob ein Serverdatensatz jemals entstanden ist. Genau dieser Abgleich ermöglicht es, die Vertrauenslücke zu erkennen, deren Schließung das gesamte Muster bezweckt.

„Absenden und vergessen” sollte sich für den Nutzer sofortig und für Sie dauerhaft anfühlen: Die Übermittlung wird lokal im Moment des Tippens auf „Absenden” erfasst, und die Zustellung ist Aufgabe des Systems, nicht des Nutzers.

Schritt 1: Übermittlung vor dem Netzwerkaufruf in IndexedDB einreihen

Speichern Sie die Übermittlung zunächst in IndexedDB, bevor Sie die Zustellung versuchen. Das Speichern vor jedem Netzwerkaufruf bedeutet, dass ein fehlgeschlagener, unterbrochener oder Offline-POST bereits gesichert ist — Sie wiederholen aus dem Speicher, niemals aus dem flüchtigen Seitenzustand. Fügen Sie jedem eingereihten Eintrag drei Felder hinzu: eine clientseitig generierte Request-ID (für spätere Deduplizierung), einen queuedAt-Zeitstempel und einen retryCount.

// db.js — a thin promise wrapper around IndexedDB
const DB_NAME = 'outbox';
const STORE = 'submissions';

function openDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, 1);
    req.onupgradeneeded = () => {
      req.result.createObjectStore(STORE, { keyPath: 'id' });
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

export async function enqueue(payload) {
  const db = await openDB();
  const item = {
    id: crypto.randomUUID(),     // request ID for idempotency
    payload,
    queuedAt: Date.now(),
    retryCount: 0,
  };
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, 'readwrite');
    tx.objectStore(STORE).put(item);
    tx.oncomplete = () => resolve(item);
    tx.onerror = () => reject(tx.error);
  });
}

crypto.randomUUID() ist in allen modernen Browsern und im Service-Worker-Kontext verfügbar, wie die MDN-Referenz zu Crypto.randomUUID() belegt. Der Formular-Handler ruft enqueue auf, zeigt einen optimistischen „In Warteschlange”-Status an und fordert anschließend einen Sync an.

Schritt 2: Background Sync registrieren und das Sync-Event behandeln

Background Sync ermöglicht es, einen benannten Tag von der Seite aus zu registrieren; der Browser löst ein sync-Event in Ihrem Service Worker aus, sobald er die Verbindung als wiederhergestellt einschätzt, und kann dieses Event sogar nach einer Navigation oder dem Schließen des Tabs auslösen. Dieses Verhalten der verzögerten Zustellung nach der Navigation macht es stärker als einen online-Listener, der nur ausgelöst wird, solange die Seite geöffnet ist.

Prüfen Sie das Vorhandensein von serviceWorker und SyncManager per Feature-Detection, bevor Sie sich darauf verlassen, und greifen Sie sofort auf den Fallback zurück, wenn eines von beiden fehlt:

// form handler, after enqueue()
async function requestSync() {
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    const reg = await navigator.serviceWorker.ready;
    try {
      await reg.sync.register('sync-forms');
      return;
    } catch {
      // registration failed — fall through to the fallback
    }
  }
  flushQueue(); // the everywhere fallback (Step 3)
}

Der register('sync-forms')-Aufruf und das SyncManager-Interface sind in der MDN-Dokumentation zur Background Synchronization API beschrieben. Im Service Worker gleichen Sie den Tag ab, kapseln die Arbeit in event.waitUntil(), damit der Worker aktiv bleibt, senden jeden Eintrag per POST, löschen ihn bei Erfolg und werfen bei einem Fehler eine Exception, damit der Browser die Registrierung beibehält und es erneut versucht:

// service-worker.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-forms') {
    event.waitUntil(replayQueue(event));
  }
});

async function replayQueue(event) {
  const items = await getAll();         // read from IndexedDB
  for (const item of items) {
    const res = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': item.id,      // dedupe on the server
      },
      body: JSON.stringify(item.payload),
    });
    if (res.ok) {
      await remove(item.id);             // delete on success
    } else if (res.status >= 500) {
      if (event.lastChance) await notifyFailure(item);
      throw new Error('server error, retry');   // keep the registration
    }
    // 4xx: the payload is bad — don't retry blindly; surface it instead
  }
}

Das Werfen einer Exception innerhalb von waitUntil ist das Signal, das den Sync ausstehend hält. Browser, die die API unterstützen, wiederholen fehlgeschlagene Anfragen in einem vom Browser verwalteten Intervall, das wahrscheinlich exponentielles Backoff zwischen den Wiederholungsversuchen verwendet. Die Anzahl der Versuche und das genaue Intervall werden vom Browser verwaltet und sind nicht vertraglich dokumentiert — treffen Sie daher keine Annahmen wie „drei Wiederholungen”. Prüfen Sie stattdessen event.lastChance — dokumentiert in der MDN-Referenz zu SyncEvent — um den letzten Versuch zu erkennen und den Nutzer darüber zu informieren, dass die Übermittlung endgültig fehlgeschlagen ist, anstatt sie still verschwinden zu lassen.

Schritt 3: Der universell funktionierende Fallback

Da Background Sync ausschließlich in Chromium-basierten Browsern verfügbar ist, ist der online-Event-Fallback keine Randnotiz — er ist die Basislösung, die in jedem Browser greift. Zwei Auslöser decken den Nicht-Chromium-Pfad ab: Die Warteschlange wird bei jedem online-Event neu geleert, und beim Laden der Seite wird sie einmalig geleert, um Übermittlungen aufzugreifen, die in einer früheren Sitzung eingereiht wurden.

// runs on the page, everywhere
export async function flushQueue() {
  if (!navigator.onLine) return;
  const items = await getAll();
  for (const item of items) {
    try {
      const res = await fetch('/api/orders', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': item.id,
        },
        body: JSON.stringify(item.payload),
      });
      if (res.ok) await remove(item.id);
    } catch {
      await bumpRetryCount(item.id);   // stays queued for the next trigger
    }
  }
}

window.addEventListener('online', flushQueue);
window.addEventListener('load', flushQueue);

Die ehrliche Einschränkung des Fallbacks: Er läuft nur, solange eine Seite Ihrer Origin geöffnet ist, da er auf seitenseitigen Events basiert. Er kann einen geschlossenen Tab nicht aufwecken, wie es Background Sync kann. Das ist der genaue Kompromiss — universelle Reichweite, schwächere Zustellungsgarantien — und der Grund, warum Sie Background Sync zuerst registrieren und erst danach auf den Fallback zurückgreifen.

Browser-Unterstützung für Background Sync im Jahr 2026

Stand Juni 2026 ist One-off Background Sync (SyncManager) eine ausschließlich Chromium-basierte API. MDN klassifiziert sie als „eingeschränkt verfügbar” — nicht als Baseline, da sie in einigen der am häufigsten genutzten Browser nicht funktioniert. Die caniuse-Daten bestätigen die nachstehende Aufteilung.

BrowserOne-off Background Sync (SyncManager)
Chrome✅ Unterstützt
Edge (Chromium)✅ Unterstützt
Opera✅ Unterstützt
Samsung Internet✅ Unterstützt
Firefox❌ Nicht unterstützt
Safari (macOS)❌ Nicht unterstützt
Safari (iOS)❌ Nicht unterstützt
Android WebView❌ Nicht unterstützt

Zwei Fallstricke sind erwähnenswert. Microsoft Edge erhielt die Unterstützung erst nach dem Wechsel zu Chromium, weshalb jede Aussage aus dem Jahr 2019, „Edge unterstütze es nicht”, inzwischen falsch ist. Und Android WebView — die Komponente, die native Apps für In-App-Browsing einbetten — stellt SyncManager nicht bereit, sodass das Einbetten einer PWA in eine WebView-Shell die native Sync-Warteschlange verliert und auf den online-Event-Pfad zurückfällt. Beachten Sie, dass es sich hierbei um die One-off-Background-Sync-API handelt; Periodic Background Sync ist eine separate, experimentelle API für wiederkehrende Aktualisierungen — verwechseln Sie die beiden nicht.

Idempotenz: Wiederholungen vor doppelten Bestellungen schützen

Jeder wiederholte POST birgt das Risiko eines Duplikats: Der erste Versuch kann den Server erreicht und committed haben, bevor die Verbindung abbrach, sodass die Antwort nie zurückkam und Ihr Client eine Anfrage wiederholt, die der Server bereits verarbeitet hat. Verhindern Sie dies mit einer clientseitig generierten Request-ID, die als Idempotency-Key-Header gesendet wird — dieselbe item.id, die Sie beim Einreihen gespeichert haben. Der Server nutzt sie als Schlüssel und gibt bei einer Wiederholung das ursprüngliche Ergebnis zurück, anstatt einen zweiten Datensatz zu erstellen.

Idempotency-Key ist eine etablierte API-Konvention, die von Stripe und PayPal popularisiert wurde und Gegenstand eines IETF Internet-Drafts ist: draft-ietf-httpapi-idempotency-key-header. Betrachten Sie es als eine in Entwicklung befindliche Konvention, nicht als ratifizierten Standard: Das HTTP-Idempotency-Key-Anforderungsheaderfeld kann verwendet werden, um nicht-idempotente HTTP-Methoden wie POST oder PATCH fehlertolerant zu machen, aber der Draft selbst trägt den Standardhinweis, dass er nur als laufende Arbeit zitiert werden darf. Die Serverseite ist unkompliziert: Bei einer doppelten Anfrage, deren Idempotenzschlüssel bereits bekannt ist, sollte der Ressourcenserver mit dem Ergebnis der zuvor abgeschlossenen Operation antworten — ob Erfolg oder Fehler.

Bestätigungs-UX nach dem Verlassen der Seite

Das schwierige UX-Problem besteht darin, dass die verzögerte Zustellung häufig abgeschlossen wird, nachdem der Nutzer bereits navigiert ist. Daher benötigen Sie einen Weg, den optimistischen „In Warteschlange”-Status abzugleichen, sobald die Anfrage tatsächlich zugestellt wurde. Drei Maßnahmen decken dies ab:

  • „In Warteschlange” → „Gesendet” live umschalten. Wenn der Service Worker einen Eintrag erfolgreich wiederholt, senden Sie per postMessage eine Nachricht an alle offenen Clients, damit die Benutzeroberfläche direkt aktualisiert werden kann. Hören Sie mit navigator.serviceWorker.addEventListener('message', ...) zu.
  • Beim nächsten Laden abgleichen. Lesen Sie beim Start den entleerten Zustand der Warteschlange: Noch vorhandene Einträge sind ausstehend; ihr Fehlen bedeutet, dass die Zustellung erfolgreich war. Rendern Sie den Status aus dem Speicher, nicht aus einem beim Absenden gesetzten Flag.
  • Endgültigen Fehler anzeigen. Verwenden Sie event.lastChance im sync-Handler, um eine Benachrichtigung auszulösen (oder einen „fehlgeschlagen”-Marker zu speichern, den die Benutzeroberfläche beim nächsten Laden liest), damit eine Übermittlung, die alle Wiederholungsversuche ausgeschöpft hat, nicht still verschwindet.

Die Bestätigungs-UX ist der zweite Bereich, in dem Session Replay seinen Wert beweist: Replay macht den nutzerseitigen Zeitverlauf sichtbar — die eingereihte Übermittlung, den angezeigten Bildschirm, ob die „Gesendet”-Bestätigung jemals gerendert wurde — was die einzige Ansicht ist, die die Vertrauenslücke aufzeigt, wenn eine Anfrage den Server nie erreicht hat und damit auch nicht protokolliert wurde.

Workbox als produktionsreife Abkürzung

Wenn Sie die Warteschlange nicht selbst implementieren möchten, bietet Workbox (aktuell Hauptversion 7) das BackgroundSyncPlugin, das fehlgeschlagene Anfragen in IndexedDB einreiht und bei sync-Events wiederholt. Entscheidend ist, dass es einen eigenen Fallback mitliefert: In Browsern, die die BackgroundSync-API nicht nativ unterstützen, versucht Workbox Background Sync automatisch eine Wiederholung, sobald Ihr Service Worker startet.

import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';

const bgSync = new BackgroundSyncPlugin('order-queue', {
  maxRetentionTime: 24 * 60, // minutes; the documented example keeps items 24h
});

registerRoute(/\/api\/orders/, new NetworkOnly({ plugins: [bgSync] }), 'POST');

Zwei Verhaltensweisen führen häufig zu Verwirrung. Erstens: BackgroundSyncPlugin greift in den fetchDidFail-Plugin-Callback ein, der nur ausgelöst wird, wenn eine Exception geworfen wird — in der Regel bei einem Netzwerkfehler. Das bedeutet, dass Anfragen nicht wiederholt werden, wenn eine Antwort mit einem 4xx- oder 5xx-Statuscode empfangen wird. Wenn Sie 5xx-Antworten erneut einreihen möchten, fügen Sie ein fetchDidSucceed-Plugin hinzu, das bei response.status >= 500 eine Exception wirft. Zweitens: Beim Testen können Sie prüfen, ob Anfragen eingereiht wurden, indem Sie in Chrome DevTools unter „Application” → „IndexedDB” nachsehen, und eine Wiederholung unter „Application” → „Service Workers” erzwingen. Validieren Sie Offline-Verhalten nicht mit dem DevTools-Kontrollkästchen „Offline” — es blockiert Seitenanfragen, lässt aber Service-Worker-Anfragen durch und verbirgt damit genau die Fehler, die Sie testen möchten. Trennen Sie stattdessen die echte Netzwerkverbindung.

Unabhängig davon, ob Sie die Lösung selbst implementieren oder Workbox verwenden, ist die Architektur dieselbe:

  1. Lokal erfassen, bevor das Netzwerk genutzt wird;
  2. Background Sync bevorzugen, wo es verfügbar ist;
  3. überall sonst auf online-Events zurückgreifen;
  4. Wiederholungen mit einem Idempotenzschlüssel deduplizieren; und
  5. die Benutzeroberfläche abgleichen, wenn die Zustellung schließlich erfolgt.

Verbinden Sie diese fünf Bausteine mit Ihrem eigenen Formular und verifizieren Sie anschließend eine echte Wiederholung, indem Sie offline einreihen, die Verbindung wiederherstellen und bestätigen, dass genau ein Serverdatensatz entstanden ist.

Häufig gestellte Fragen

Was ist der Unterschied zwischen One-off Background Sync und Periodic Background Sync?

One-off Background Sync verwendet das SyncManager-Interface, um eine einzelne zurückgestellte Aufgabe — wie eine eingereihte Formularübermittlung — einmalig zu wiederholen, sobald die Verbindung wiederhergestellt ist; der Browser löst ein sync-Event aus, das auch nach einer Navigation des Nutzers eintreffen kann. Periodic Background Sync verwendet ein separates PeriodicSyncManager-Interface für wiederkehrende Aktualisierungen nach einem browserseitig verwalteten Zeitplan, etwa zum Aktualisieren von Inhalten. Es handelt sich um unterschiedliche APIs, und Periodic Background Sync ist experimentell — prüfen Sie daher die Kompatibilitätstabelle, bevor Sie es in der Produktion einsetzen.

Warum wird meine eingereihte Anfrage bei einer 400- oder 500-Antwort des Servers nicht wiederholt?

Background Sync und das BackgroundSyncPlugin von Workbox wiederholen nur Anfragen, die aufgrund eines echten Netzwerkfehlers fehlschlagen, da das Plugin in den fetchDidFail-Callback eingreift, der nur ausgelöst wird, wenn eine Exception geworfen wird. Eine 400- oder 500-Antwort gilt als empfangene Antwort und wird daher als zugestellt gewertet und nicht wiederholt. Um 5xx-Antworten erneut einzureihen, fügen Sie ein fetchDidSucceed-Plugin hinzu, das eine Exception wirft, wenn response.status 500 oder höher ist, oder werfen Sie in einem selbst implementierten sync-Handler manuell eine Exception.

Funktioniert Background Sync in einem Android WebView, das eine PWA einbettet?

Nein. Android WebView, die Komponente, die native Apps für In-App-Browsing einbetten, stellt SyncManager nicht bereit, sodass eine in eine WebView-Shell eingebettete PWA die native Background-Sync-Warteschlange verliert. Die Übermittlung fällt auf den online-Event-Pfad zurück, der nur ausgelöst wird, solange eine Seite Ihrer Origin geöffnet ist, und kann einen geschlossenen Tab nicht aufwecken. Aus diesem Grund und aufgrund der fehlenden Unterstützung in Firefox, Safari und iOS muss eine IndexedDB-Warteschlange mit einem online-Event-Fallback die Basislösung bleiben.

Wie kann ich das Offline-Background-Sync-Verhalten zuverlässig testen?

Trennen Sie die tatsächliche Netzwerkverbindung, anstatt das DevTools-Kontrollkästchen 'Offline' zu verwenden. Das Offline-Kontrollkästchen blockiert Anfragen von der Seite, lässt aber Service-Worker-Anfragen passieren und verbirgt damit genau die Fehler, die Sie testen möchten. Überprüfen Sie eingereihte Einträge in Chrome DevTools unter 'Application' → 'IndexedDB' und erzwingen Sie eine Wiederholung unter 'Application' → 'Service Workers'. Um den vollständigen Ablauf zu bestätigen, reihen Sie eine Übermittlung offline ein, stellen Sie die Verbindung wieder her und prüfen Sie, ob genau ein Serverdatensatz entstanden ist.

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.