wenn die App auch ohne Netz funktionieren muss
Ein Netzausfall darf keine tote App bedeuten. Mit Cache Persistence, einer Mutation-Queue für ausstehende Änderungen und durchdachter Konfliktauflösung lässt sich eine GraphQL-Anwendung so bauen, dass Nutzer weiterlesen, weiterarbeiten und Änderungen vornehmen können, während im Hintergrund auf die Rückkehr der Verbindung gewartet wird.
Inhaltsverzeichnis
- 1. Was Offline-First bei GraphQL wirklich bedeutet
- 2. Cache Persistence: den Apollo-Cache über Neustarts hinweg erhalten
- 3. Netzwerkstatus erkennen und Apollo Client informieren
- 4. Mutation Queue: Änderungen offline sammeln
- 5. Optimistic Responses als Basis für Offline-UX
- 6. Konfliktauflösung beim Reconnect
- 7. Speicherlimits und selektive Persistenz
- 8. Offline-Verhalten testen, ohne den Flughafenmodus zu bemühen
- 9. Offline-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Offline-First bei GraphQL wirklich bedeutet
Offline-First GraphQL bedeutet nicht, dass eine App im Flugmodus alle Funktionen bietet, sondern dass der lokale Cache als primäre Datenquelle behandelt wird und das Netzwerk als Synchronisationsmechanismus im Hintergrund. Der Unterschied ist grundlegend: Statt bei jedem Screen-Aufruf auf eine Netzwerkantwort zu warten, liest die App zuerst aus dem lokalen Apollo-Cache und aktualisiert diesen, sobald neue Daten verfügbar sind. Für den Nutzer fühlt sich das an, als würde die App nie warten, unabhängig davon, ob gerade eine Verbindung besteht.
Apollo Client bringt für diesen Ansatz bereits eine wichtige Grundlage mit: einen normalisierten In-Memory-Cache, der Objekte anhand ihrer ID referenziert und Queries daraus bedient. Für Offline-First GraphQL reicht ein reiner In-Memory-Cache aber nicht aus, weil er beim App-Neustart verloren geht. Erst die Kombination aus Cache Persistence, also dem Sichern des Caches auf dem Gerät, und einer Mutation-Queue für ausstehende Änderungen macht eine App wirklich offline-tauglich.
Wichtig ist auch die Erwartungshaltung: Nicht jede Funktion lässt sich sinnvoll offline anbieten. Ein Live-Chat oder eine Zahlungsfreigabe brauchen zwingend eine aktive Verbindung. Offline-First GraphQL zielt vor allem auf Lesezugriffe, die aus dem Cache bedient werden können, und auf Schreibzugriffe, die sich verzögert nachreichen lassen, ohne dass der Nutzer auf ein sofortiges Server-Ergebnis angewiesen ist. Die folgenden Abschnitte zeigen, wie sich diese Bausteine mit Apollo Client konkret umsetzen lassen.
2. Cache Persistence: den Apollo-Cache über Neustarts hinweg erhalten
Der erste Baustein von Offline-First GraphQL ist, den Apollo-Cache nicht nur im Arbeitsspeicher zu halten, sondern regelmäßig auf einen persistenten Speicher zu schreiben. Das Paket apollo3-cache-persist übernimmt genau diese Aufgabe: Es serialisiert den InMemoryCache und speichert ihn in AsyncStorage auf React Native oder localStorage im Web, mit konfigurierbarem Debounce, damit nicht jede einzelne Cache-Änderung sofort einen teuren Schreibvorgang auslöst.
Beim App-Start wird der gespeicherte Cache vor dem ersten Rendern wiederhergestellt. Das bedeutet, dass ein Nutzer, der die App im Offline-Zustand öffnet, sofort die zuletzt gesehenen Daten sieht, statt einen Ladezustand oder eine Fehlermeldung. Für Offline-First GraphQL ist dieser Schritt die Grundvoraussetzung, ohne persistenten Cache gibt es nach einem Neustart schlicht keine Daten, die offline angezeigt werden könnten.
// Apollo Client with persisted cache, restored before first render
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { persistCache, AsyncStorageWrapper } from 'apollo3-cache-persist';
const cache = new InMemoryCache();
export async function createApolloClient() {
// Restore the cache from disk before the client is used anywhere
await persistCache({
cache,
storage: new AsyncStorageWrapper(AsyncStorage),
debounce: 500, // ms, batches rapid cache writes
maxSize: 5 * 1024 * 1024, // 5 MB persisted cache limit
});
return new ApolloClient({
uri: 'https://api.mironsoft.de/graphql',
cache,
});
}
3. Netzwerkstatus erkennen und Apollo Client informieren
Ein persistenter Cache allein reicht nicht, die App muss auch wissen, ob gerade eine Verbindung besteht, um Requests entsprechend zu steuern. Das Paket @react-native-community/netinfo liefert Verbindungsstatus-Updates in Echtzeit. Apollo Client wiederum bietet in Kombination mit apollo-link-queue oder eigenen Custom Links die Möglichkeit, ausgehende Operationen zu pausieren, solange keine Verbindung besteht, statt sie fehlschlagen zu lassen.
Für Offline-First GraphQL ist dieser Mechanismus entscheidend: Statt einer Fehlermeldung bei jedem Request ohne Netz wird die Operation in eine Warteschlange gestellt und automatisch erneut versucht, sobald die Verbindung zurückkehrt. Das verändert die Nutzererfahrung fundamental, aus "Fehler, bitte erneut versuchen" wird "wird synchronisiert, sobald wieder online".
// Pause outgoing operations while offline, resume automatically on reconnect
import NetInfo from '@react-native-community/netinfo';
import { RetryLink } from '@apollo/client/link/retry';
import QueueLink from 'apollo-link-queue';
const queueLink = new QueueLink();
NetInfo.addEventListener((state) => {
// Close the queue while offline, open it again once reconnected
queueLink.close();
if (state.isConnected) {
queueLink.open();
}
});
const retryLink = new RetryLink({
delay: { initial: 1000, max: 30000, jitter: true },
attempts: { max: 5 },
});
// Link order matters: queue first, then retry, then the actual transport
export const offlineLink = queueLink.concat(retryLink);
4. Mutation Queue: Änderungen offline sammeln
Lesezugriffe lassen sich relativ einfach aus dem persistenten Cache bedienen, Schreibzugriffe sind der schwierigere Teil von Offline-First GraphQL. Wenn ein Nutzer offline eine Bestellung aufgibt oder eine Notiz bearbeitet, muss diese Mutation gespeichert werden, bis wieder eine Verbindung besteht. Eine Mutation Queue speichert dafür ausstehende Operationen mit ihren Variablen persistent, meist in derselben Storage-Schicht wie der Cache selbst.
Wichtig ist, dass jede Mutation in der Queue idempotent gestaltet wird, also mehrfach sicher ausgeführt werden kann, ohne doppelte Effekte zu erzeugen. Ein client-generierter, eindeutiger Idempotency-Key pro Mutation verhindert, dass eine Bestellung doppelt angelegt wird, falls der Reconnect-Prozess eine Mutation versehentlich zweimal absendet, etwa durch eine kurze Netzwerkunterbrechung mitten in der Synchronisation.
// Simplified mutation queue with idempotency keys
import { v4 as uuid } from 'uuid';
interface QueuedMutation {
id: string; // idempotency key, generated once per mutation
mutation: string; // serialized GraphQL document
variables: Record<string, unknown>;
createdAt: number;
}
async function enqueueMutation(mutation: string, variables: Record<string, unknown>) {
const queued: QueuedMutation = {
id: uuid(),
mutation,
variables: { ...variables, idempotencyKey: uuid() },
createdAt: Date.now(),
};
const existing = JSON.parse((await AsyncStorage.getItem('mutationQueue')) ?? '[]');
await AsyncStorage.setItem('mutationQueue', JSON.stringify([...existing, queued]));
}
async function flushQueue(client: ApolloClient<object>) {
const queue: QueuedMutation[] = JSON.parse(
(await AsyncStorage.getItem('mutationQueue')) ?? '[]'
);
for (const item of queue) {
// Server checks idempotencyKey and returns the existing result on retry
await client.mutate({ mutation: gql(item.mutation), variables: item.variables });
}
await AsyncStorage.removeItem('mutationQueue');
}
5. Optimistic Responses als Basis für Offline-UX
Damit sich eine Mutation offline sofort im UI widerspiegelt, statt auf die tatsächliche Server-Antwort zu warten, kombiniert Offline-First GraphQL die Mutation Queue mit Apollo Clients Optimistic-Response-Mechanismus. Der Client wendet das erwartete Ergebnis sofort lokal auf den Cache an, sobald der Server erreichbar ist und antwortet, wird das optimistische Ergebnis durch das echte ersetzt. Für den Nutzer verschwindet dadurch der Unterschied zwischen online und offline fast vollständig, eine Notiz erscheint sofort in der Liste, unabhängig vom aktuellen Verbindungsstatus.
Der Haken dabei: Optimistic Responses funktionieren nur gut, wenn das erwartete Serverergebnis vorhersagbar ist, etwa eine neue ID, die der Client selbst generiert und dem Server als Vorschlag mitgibt. Bei Operationen mit serverseitig berechneten Werten, etwa einem finalen Preis nach Rabattlogik, sollte die optimistische Anzeige klar als vorläufig markiert werden, damit Nutzer nicht überrascht werden, wenn sich der Wert nach der Synchronisation noch leicht ändert.
6. Konfliktauflösung beim Reconnect
Sobald mehrere Geräte oder ein Gerät nach längerer Offline-Zeit wieder synchronisieren, können Konflikte entstehen: Der Server-Zustand hat sich verändert, während lokale Mutations noch ausstehen. Offline-First GraphQL braucht deshalb eine bewusste Konfliktstrategie, statt sich blind auf "letzter Schreibvorgang gewinnt" zu verlassen, was in der Praxis oft zu stillem Datenverlust führt. Für additive Operationen, etwa das Hinzufügen eines Kommentars, ist ein Konflikt selten problematisch. Für Operationen, die einen Wert überschreiben, etwa einen Bestellstatus, ist eine explizite Versionsprüfung sinnvoll.
Ein bewährtes Muster ist, jede änderbare Entität mit einer Versionsnummer oder einem Zeitstempel zu versehen und diesen bei der Mutation mitzuschicken. Der Server vergleicht die mitgeschickte Version mit dem aktuellen Stand und lehnt die Mutation ab, wenn sich die Daten inzwischen geändert haben, statt sie stillschweigend zu überschreiben. Der Client kann diesen Fall dann gezielt behandeln, etwa mit einem Merge-Dialog oder einer klaren Fehlermeldung, statt den Konflikt zu verschleiern.
7. Speicherlimits und selektive Persistenz
Mobile Geräte haben begrenzten Speicherplatz, und ein unbegrenzt wachsender Apollo-Cache ist keine gute Idee. Für Offline-First GraphQL lohnt es sich, die Persistenz gezielt auf die Daten zu beschränken, die tatsächlich offline gebraucht werden, statt den kompletten Cache blind zu sichern. Das persistCache-Setup erlaubt eine maxSize-Begrenzung, zusätzlich lassen sich über Cache-Eviction-Policies gezielt selten genutzte Einträge entfernen, bevor der Speicher vollläuft.
Ein sinnvolles Muster ist, zwischen kritischen Daten, die für die Kernfunktionalität offline gebraucht werden, und optionalen Daten wie selten aufgerufenen Detailseiten zu unterscheiden. Kritische Daten werden aggressiv vorab geladen und persistiert, optionale Daten bleiben im flüchtigen In-Memory-Cache und werden bei fehlendem Speicherplatz zuerst entfernt. Diese Priorisierung verhindert, dass wichtige Offline-Funktionalität durch unwichtige Detaildaten verdrängt wird.
8. Offline-Verhalten testen, ohne den Flughafenmodus zu bemühen
Offline-Verhalten manuell zu testen, indem man ständig den Flugmodus umschaltet, ist mühsam und deckt selten alle relevanten Fälle ab. Für Offline-First GraphQL lohnt sich ein simulierter Netzwerkstatus in Tests, der gezielt Szenarien wie "Mutation startet online, Verbindung bricht mitten in der Anfrage ab, Reconnect nach zehn Sekunden" reproduzierbar macht. Mock-Implementierungen von NetInfo und ein konfigurierbarer Custom Link, der Requests künstlich verzögert oder fehlschlagen lässt, ermöglichen automatisierte Tests dieser Abläufe.
Besonders wichtig sind Tests für die Mutation Queue selbst: Bleibt die Reihenfolge der Mutations beim Flush erhalten, werden Idempotency-Keys korrekt wiederverwendet bei Retries, und wird die Queue nach erfolgreichem Flush wirklich geleert. Diese Randfälle fallen in manuellen Tests häufig durch, weil sie sehr spezifisches Timing brauchen, das sich am Gerät kaum zuverlässig reproduzieren lässt.
9. Offline-Strategien im Vergleich
Je nach Anforderung eignen sich unterschiedliche Kombinationen der vorgestellten Bausteine. Die folgende Übersicht ordnet die wichtigsten Strategien nach Komplexität und Robustheit.
| Strategie | Offline-Lesen | Offline-Schreiben | Konfliktsicherheit |
|---|---|---|---|
| Nur In-Memory-Cache | Nein (nach Neustart) | Nein | Nicht relevant |
| Cache Persistence allein | Ja | Nein | Nicht relevant |
| Persistence + Mutation Queue | Ja | Ja | Nur additiv sicher |
| + Versionsprüfung | Ja | Ja | Hoch |
Für die meisten Anwendungen ist die Kombination aus Cache Persistence und Mutation Queue mit Idempotency-Keys bereits ein solides Fundament für Offline-First GraphQL. Versionsprüfung lohnt sich zusätzlich immer dann, wenn dieselbe Entität von mehreren Geräten oder Nutzern gleichzeitig verändert werden kann.
Mironsoft
GraphQL-Architektur und Offline-fähige Apps
App soll auch ohne Netz zuverlässig funktionieren?
Wir richten Cache Persistence, Mutation Queue und Konfliktauflösung mit Apollo Client ein und testen das Offline-Verhalten gezielt, statt es dem Zufall zu überlassen.
Cache-Strategie
Persistenz-Setup mit sinnvollen Speicherlimits und Priorisierung
Mutation Queue
Idempotente, persistente Warteschlange für Offline-Mutations
Konfliktauflösung
Versionsprüfung und Merge-Strategien für parallele Änderungen
10. Zusammenfassung
Offline-First GraphQL mit Apollo Client baut auf drei Säulen: Cache Persistence sichert bereits gesehene Daten über Neustarts hinweg, eine Mutation Queue sammelt ausstehende Schreibzugriffe idempotent, und Optimistic Responses lassen Änderungen sofort im UI erscheinen, unabhängig vom aktuellen Verbindungsstatus. Netzwerkstatus-Erkennung über NetInfo steuert, wann Requests pausiert und wann sie automatisch erneut versucht werden.
Konfliktauflösung über Versionsprüfung verhindert, dass parallele Änderungen sich stillschweigend überschreiben, und selektive Persistenz mit klaren Speicherlimits hält die App auch bei begrenztem Gerätespeicher funktionsfähig. Wer diese Bausteine kombiniert, statt einzeln zu betrachten, bekommt eine App, die sich für Nutzer nahezu identisch anfühlt, egal ob gerade eine Verbindung besteht oder nicht.
Offline-First GraphQL — Das Wichtigste auf einen Blick
Cache Persistence
apollo3-cache-persist sichert den normalisierten Cache in AsyncStorage, wiederhergestellt vor dem ersten Render.
Mutation Queue
Ausstehende Mutations mit Idempotency-Keys persistent speichern, bis die Verbindung zurückkehrt.
Optimistic UI
Sofortige lokale Anzeige des erwarteten Ergebnisses, ersetzt durch die echte Serverantwort nach Sync.
Konfliktauflösung
Versionsprüfung statt "letzter gewinnt" verhindert stillen Datenverlust bei parallelen Änderungen.