Startseite / Artikel

Wie die Browser-Speicherung heimlich Benutzerdaten zerstört und wie man dies verhindern kann

Die Speicherung im Browser kann Benutzerdaten ohne Fehler beschädigen: verlorene Updates, veraltete Snapshots, Schema-Veränderungen und gemeinsam genutzte Geräte. Praktische Ansätze für localStorage, die standhalten.

Verlag: REACTAPP.TOP14 Min. Lesezeit3109 Wörter

Quellen: The Browser Storage Mistake That Quietly Corrupts User Data

Der Speicher im Browser ist erfrischend einfach zu nutzen. Serialisieren Sie ein Objekt mit JSON.stringify, lesen Sie es mit JSON.parse wieder ein – und die Präferenzen des Benutzers, das halb ausgefüllte Formular, Filter oder Entwürfe bleiben beim nächsten Laden erhalten. Kein Schema, keine Netzwerkanfrage, keine Migration im Backend. Ein paar Zeilen JavaScript reichen aus, um einer Anwendung innerhalb weniger Minuten eine persistente Wirkung zu verleihen.

Diese Einfachheit führt zu der riskanten Annahme, dass alles, was im Browser gespeichert wird, als kleine lokale Datenbank betrachtet werden kann.

Das Problem beginnt in der Regel mit einem großen Zustandsobjekt. Die Anwendung lädt das gesamte Objekt aus localStorage herunter, ändert eine Eigenschaft im Speicher und schreibt das gesamte Objekt wieder zurück. Jeder Schritt scheint vernünftig zu sein: Der JSON-Code ist gültig, die Schreibvorgang ist erfolgreich und der Bildschirm zeigt den erwarteten Wert. Nichts scheint fehlerhaft zu sein.

Probleme treten auf, wenn ein anderer Teil der App eine veraltete Kopie desselben Objekts enthält. Vielleicht ändert ein zweiter Tab eine Einstellung, eine Hintergrundaufgabe bearbeitet einen Entwurf, oder eine frühere Version der App interpretiert das Objekt auf andere Weise. Möglicherweise lädt ein Komponente alte Daten gerade nachdem der Benutzer bereits etwas geändert hat. Da jeder dieser Schreibprozesse ein vollständiges Snapshot speichert, kann die Änderung eines einzigen Feldes heimlich alte Werte in allen anderen Feldern wiederherstellen.

Es ist weder ein Syntaxfehler erforderlich noch wird eine Speicherungsexception ausgelöst. Jeder Schreibprozess kann perfekt gültige Daten speichern und dennoch die gültige Aktualisierung eines anderen Schreibprozesses überschreiben.

Die Storage-API ist eigentlich nicht der Schuldige. Der Fehler besteht darin, von einem einfachen String-Speicher, bei dem die letzte Schreiboperation gilt, zu erwarten, dass er sich wie ein System verhält, das von Datensätzen, Feldern, Transaktionen, Versionen, Eigentumsrechten und gleichzeitigem Zugriff weiß. Er kennt nichts davon und merkt sich einfach nur noch den String, der zuletzt einer Schlüssel zugewiesen wurde.

Das unschuldige Lesen-Ändern-Schreiben

Eine typische Implementierung sieht so aus:

function updatePreferences(changes) {
  const saved = localStorage.getItem("user-settings");
  const settings = saved ? JSON.parse(saved) : {};

  const nextSettings = {
    ...settings,
    ...changes,
  };
  localStorage.setItem(
    "user-settings",
    JSON.stringify(nextSettings)
  );
}

Durch ein einziges Tab und eine einzige aktuelle Version der App kann diese jahrelang laufen. Jede Aktualisierung liest den neuesten Wert, fügt die Änderungen zusammen und schreibt das Ergebnis. Der Code ist kurz, leicht verständlich und in der Regel ausreichend für grundlegende Einstellungen.

Das Risiko entsteht, wenn Zeit zwischen dem Lesen und Schreiben vergeht oder wenn die App eine Kopie des Objekts im Arbeitsspeicher behält.

Nehmen wir an, zwei Tabs laden dieselben Einstellungen:

{
  "theme": "light",
  "fontSize": 16,
  "sidebarCollapsed": false
}

Die erste Registerkarte wechselt das Design auf dunkel. Die zweite erhöht die Schriftgröße auf achtzehn. Wenn jede Registerkarte ihre eigene Zwischenspeicherkopie bearbeitet und das gesamte Objekt speichert, löscht diejenige, die zuletzt speichert, die Änderung der anderen.

Die erste Registerkarte könnte Folgendes speichern:

{
  "theme": "dark",
  "fontSize": 16,
  "sidebarCollapsed": false
}

Später speichert die zweite Registerkarte, die weiterhin das ältere Snapshot enthält, Folgendes:

{
  "theme": "light",
  "fontSize": 18,
  "sidebarCollapsed": false
}

Die Änderung der Schriftgröße bleibt bestehen, und das Theme wechselt leise zurück auf hell.

Das ist eine Korruption durch fehlgeschlagene Aktualisierung. Das endgültige Objekt ist nicht fehlerhaft, und jedes Feld enthält einen gültigen Wert. Das Problem ist, dass eine gültige Aktualisierung spurlos verschwunden ist, ohne Anzeichen dafür, dass es zu einem Konflikt gekommen wäre.

Diese Fehler bleiben während der Entwicklung verborgen, weil Entwickler nacheinander mit einem Tab, einem Gerät und einer Aktion testen. Reale Nutzer halten Apps stundenlang geöffnet, duplizieren Tabellen, stellen Sitzungen wieder her, wechseln zwischen Fenstern und kehren zu Seiten zurück, deren Zustand im Arbeitsspeicher älter ist als der aktuell gespeicherte Zustand.

Die Speicherschnittstelle weiß nicht, dass ein Tab nur das Theme und ein anderer nur die Schriftgröße geändert hat. Sie sieht zwei vollständige Zeichenketten, und die letzte gewinnt.

Ein Snapshot ist nicht der aktuelle Zustand

Der größere Fehler besteht darin, anzunehmen, dass ein aus dem Speicher geladenes Objekt weiterhin mit dem Speicher verbunden bleibt. Das ist nicht der Fall. JSON.parse erstellt zu einem bestimmten Zeitpunkt einen im Speicher liegenden Wert, der den gespeicherten String darstellt. Danach handelt es sich lediglich um einen Schnappschuss. In der Zwischenzeit kann eine andere Registerkarte den Schlüssel aktualisieren, ein Komponente ihn ersetzen oder eine neuere App-Version ihn migrieren – doch die bereits im Speicher vorhandene Kopie hat davon keine Ahnung.

State-Management verschärft die Situation noch. Eine App kann beim Starten einen Store aus localStorage laden und anschließend den gesamten Store nach jeder Änderung persistieren. Mehrere Komponenten arbeiten dabei mit diesem geladenen Schnappschuss, während externe Schreibvorgänge denselben Schlüssel ändern.

Verzögerte Speicherungen erweitern das Zeitfenster. Viele Apps deaktivieren die sofortige Persistenz, sodass localStorage.setItem bei jeder Tastenbetätigung nicht ausgeführt wird. Der Benutzer ändert ein Feld, die Speicherung wird einige hundert Millisekunden später geplant, und die Klammerfunktion bewahrt den Zustandsobjekt von diesem Moment auf. Bevor die Speicherung erfolgt, kann eine andere Registerkarte oder Komponente neuere Daten speichern. Wenn die Callback-Funktion schließlich ausgeführt wird, schreibt sie ihr älteres Snapshot über die neueren Werte.

Die Deaktivierung der sofortigen Ausführung verbesserte die Leistung, beeinträchtigte aber die Konsistenz, da niemand festlegte, was mit veralteten Snapshots geschehen soll.

Asynchrone Initialisierung erzeugt dasselbe Muster. Ein Komponente wird mit den Standardwerten dargestellt, der Benutzer interagiert sofort, und der gespeicherte Zustand kommt später über einen Framework-Effekt oder eine Abstraktion. Der Hydratierungs-Schritt ersetzt anschließend den aktuellen Zustand durch ein älteres gespeichertes Snapshot. Für den Benutzer scheint es, als würde die Benutzeroberfläche seine Änderung kurz nach deren Vornahme rückgängig machen.

Derartige Fehler wirken zufällig, weil jeder beteiligte Wert gültig ist. Das eigentliche Problem liegt in der zeitlichen Priorität: Soll der früher geladene Wert Vorrang haben, die spätere Benutzeraktion oder der von einem anderen Kontext geschriebene Eintrag? Wenn keine Regel festgelegt ist, entscheidet das, was zufällig zuletzt ausgeführt wird.

Persistenz ohne Transaktionen

localStorage wird in der Regel als synchroner Schlüssel-Wert-Speicher beschrieben. Das stimmt, doch es weckt mehr Vertrauen, als die API verdient.

Ein Aufruf von ersetzt einen Wert synchron im aktuellen Kontext. Diese Atomarität gilt nur für einen einzigen Aufruf. Eine Abfolge aus Lesen, Ändern und Schreiben, die sich über mehrere Schritte erstreckt, ist zwischen Tabs oder Worker-Prozessen nicht transaktional. Es gibt weder Vergleichen und Austauschen, noch Spaltenverschlüsselung, keine Versionskonflikte, keine Zusammenführung auf Feldebene und keinen Rollback.

Konzeptionell bleibt die Abfolge dennoch:

const current = read();
const next = modify(current);
write(next);

Ein anderer Kontext kann seine eigene Abfolge gegen einen anderen Snapshot ausführen. Jeder Lese- und Schreibvorgang ist erfolgreich, doch das kombinierte Ergebnis führt zum Verlust von Daten.

Durch die Aufteilung von Kategorien in separate Schlüssel wird der Konflikt verringert. Ein Thema, das unter einem Schlüssel gespeichert ist, kann keinen nicht verwandten Entwurf unter einem anderen Schlüssel überschreiben. Das ist besser als das Speichern eines einzigen riesigen Zustandsobjekts, obwohl es nicht alle Konkurrenzprobleme löst, da zwei Benutzer weiterhin um denselben Entwurf oder dieselbe Präferenzdatei kämpfen können.

Der Browser sendet außerdem ein storage-Event aus, um anderen Dokumenten mitzuteilen, wenn sich ein Schlüssel in localStorage ändert. Dies unterstützt die Synchronisierung, ist aber kein Transaktionsmechanismus. Sobald eine andere Tabellenansicht das Event erhält, hat sie möglicherweise bereits auf einen veralteten Zustand reagiert oder einen Schreibvorgang geplant, der ihre ältere Version wiederherstellt. Das Event meldet, dass ein weiterer Schreibvorgang stattgefunden hat. Die Anwendung muss dennoch entscheiden, wie sie diesen Vorgang mit ungespeicherten lokalen Änderungen in Einklang bringen soll.

Hier werden viele Implementierungen kompliziert. Entwickler fügen Zuhörer hinzu, verschmelzen Objekte, aktualisieren Speicher und unterdrücken zyklische Schreibvorgänge – und schon haben sie ein teilweises Synchronisierungssystem auf einer API aufgebaut, die gewählt wurde, weil sie einfacher erschien als eine echte Datenlage.

Das bedeutet nicht, dass localStorage keine Anwendungsdaten speichern sollte. Es bedeutet vielmehr, dass die von Ihnen benötigten Garantien mit dem übereinstimmen müssen, was die API bietet. Für kleine Werte mit einem klaren Eigentümer und Toleranz gegenüber Überschreibungen eignet es sich gut. Es wird jedoch anfällig, wenn mehrere Benutzer unabhängig voneinander ein gemeinsames, strukturiertes Datensatz bearbeiten.

Ein großer JSON-Block hat einen großen Einflussbereich

Es ist praktisch, den gesamten Zustand unter einem Schlüssel zu speichern, da die Wiederherstellung nur einen einzigen Lese- und Parseschritt erfordert. Das bedeutet jedoch auch, dass jede Aktualisierung alles überschreibt.

Eine Anpassung einer Seitenleisten-Einstellung führt dazu, dass alles andere, das denselben Datensatz teilt, neu geschrieben wird: der unvollendete Entwurf, Authentifizierungsmetadaten, aktuelle Suchanfragen, abgelehnte Werbebanner sowie in Cache gespeicherte Formularwerte. Falls der Verfasser für einen dieser Werte veraltete Daten besitzt, setzt die unzusammenhängende Bearbeitung den alten Wert wieder her. Je mehr Themen in einer einzigen Schlüsselgruppe zusammengefasst sind, desto wahrscheinlicher ist es, dass ein Verfasser die Änderungen eines anderen nicht gesehen hat.

Auch Migrationen leiden darunter. Eine neue Version kann einen Teil des Objekts umgestalten und den Rest unverändert lassen. Ist die Migration unvollständig, kann die Anwendung ein Objekt erstellen, das Felder aus verschiedenen Versionen mischt, und ein älteres Tab kann später seine ursprüngliche Struktur speichern und so im Grunde die Migration rückgängig machen.

Durch die Trennung der Speicherung nach Eigentümer und Lebenszyklus werden solche Konflikte verringert. Themeneinstellungen, temporäre Entwürfe, abgelehnte Benachrichtigungen sowie Cache-Einträge müssen keine gemeinsame Version, eine Ablaufregel oder einen Schreibweg teilen. Unabhängige Schlüssel erschweren es, dass eine Änderung in einem Bereich die in einem anderen Bereich verursacht wird.

Auch die Aufteilung sollte nicht mechanisch erfolgen. Das Erstellen eines Schlüssels für jede kleine Eigenschaft wird unübersichtlich und kann weiterhin widersprüchliche Kombinationen erzeugen, wenn mehrere Werte gleichzeitig geändert werden müssen. Eine gute Trennlinie ist ein Zustandsobjekt mit einem einzigen Eigentümer, einem einzigen Schema sowie einer einzigen Regel für Aktualisierungen. Werte, die als Ganzes konsistent sein müssen, können zusammengehören. Werte, die aus unabhängigen Gründen ändern, profitieren von der gemeinsamen Speicherung eines Snapshots nicht, sondern sind nur zusätzlich anfällig.

Betrachten Sie einen Schlüssel als Träger eines zusammenhängenden Datensatzes. Er sollte nicht die gesamte Speichermenge der Anwendung ersetzen.

Schema-Drift verwandelt alte Daten in neue Korruptionen

Daten im Browser überdauern oft die ursprünglichen Erwartungen. Ein Benutzer kann dasselbe Profil über Dutzende von Versionen hinweg beibehalten, und Werte, die von einer älteren Version gespeichert wurden, können Monate später wieder auftauchen. Daher verfügt der Browser-Speicher über ein Schema – auch wenn niemand eines entworfen hat.

Nehmen wir an, eine ältere Version hat einen Booleschen Wert gespeichert:

{
  "notifications": true
}

Eine neuere Version ersetzt es durch ein umfangreicheres Objekt:

{
  "notifications": {
    "email": true,
    "push": false
  }
}

Code, der davon ausgeht, dass die neue Struktur bereits vorhanden ist, ohne die alte Wertung zu überprüfen oder umzusetzen, kann Fehler verursachen. Noch heimtückischer ist es, wenn er Standardwerte einsetzt und diese später über den gespeicherten Datensatz schreibt, wodurch die zuvor von dem Benutzer gewählten Werte verworfen werden.

Umgeben von Feldern führt zu ähnlichen Problemen. Ein verbliebenes darkMode-Flag kann neben einem neueren theme-Wert existieren. Ein Teil der Anwendung liest das neue Feld, ein älterer Tab schreibt die alte Struktur – dadurch ändern sich die Benutzerpräferenzen zwischen Sitzungen unvorhersehbar.

TypeScript behebt dieses Problem nicht. Der Compiler kann zwar die aktuell erwartete Struktur beschreiben, doch JSON.parse gibt Daten aus der Laufzeit zurück, die durch unbekannten Code in der Vergangenheit erzeugt wurden. Eine Typbehauptung schweigt den Compiler und erschwert es, solche Unstimmigkeiten zu erkennen:

const settings =
  JSON.parse(savedValue) as CurrentSettings;

Diese Zeile wandelt alte Speicherdaten nicht in das aktuelle Schema um. Sie weist TypeScript lediglich an, ihnen zu vertrauen.

Sichererer Code behandelt persistierte Werte als externe Eingaben. Er analysiert sie, überprüft ihre Struktur, prüft die Version und migriert bewusst bekannte ältere Formate. Wenn ein Wert nicht sicher interpretiert werden kann, sollte die Anwendung entscheiden, ob sie ihn verwirft, zur Wiederherstellung aufbewahrt oder den Benutzer um Rat bittet.

Eine neben den Daten gespeicherte Versionnummer macht die Übergänge klar erkennbar:

{
  "version": 3,
  "data": {
    "theme": "dark"
  }
}

Eine Versionnummer führt die Migration nicht durch, gibt aber an, welche Annahmen weiterhin gelten.

Erfolgreiches Parsen bedeutet noch keine Sicherheit

Entwickler umgeben oft JSON.parse mit try/catch und rufen die Speicherschicht geschützt auf. Dadurch werden fehlerhafte JSON-Daten behandelt, was hilft, doch syntaktische Gültigkeit ist nur die erste Voraussetzung.

Ein parsierter Objekt kann fehlende erforderliche Felder aufweisen, unmögliche Werte enthalten, einem veralteten Schema folgen oder durch Entwicklerwerkzeuge oder Erweiterungen manuell bearbeitet worden sein. Auch können Werte den richtigen Typ haben, aber die falsche Bedeutung. Eine gespeicherte Seitengröße von -500 ist eine gültige Zahl innerhalb gültigen JSONs. Ein Theme mit dem Namen "ultraviolet" ist eine gültige Zeichenkette. Ein Entwurf, dem die ID fehlt, die ihn mit einem Dokument verbindet, ist ein gültiges Objekt. Keines davon verdient automatisch Vertrauen als Anwendungsstatus, denn der Browser speichert nur Zeichenketten und kennt nichts über Ihre Domänenvorschriften.

Ein zuverlässiges Hydratierungsverfahren überprüft, was die Anwendung akzeptieren will, indem es eine Schema-Bibliothek, einen kleinen handgeschriebenen Parser oder sorgfältig entworfene Konstruktoren verwendet. Entscheidend ist, dass die Überprüfung ein bekanntes internes Modell ergibt, anstatt rohe, parsierte Daten direkt in die Anwendung einzuspeisen.

Die Validierung ist auch der Ort, an dem Standardwerte verwendet werden sollten. Eine fehlende, optionale Einstellung kann sicherlich mit einem Standardwert ersetzt werden. Ein fehlender Identifikator in einem ungespeicherten Entwurf kann bedeuten, dass dieser Entwurf nicht an ein Dokument angehängt werden kann und daher nicht automatisch wiederhergestellt werden sollte. Beides als harmlose Abwesenheit zu betrachten, schafft ein falsches Vertrauen, das Korruptionen verbirgt. Ein Ersatzwert sollte Unsicherheiten nicht verbergen. Wenn die App nicht nachweisen kann, dass die gespeicherten Daten dem aktuellen Benutzer sowie dem Schema und der Datenspur gehören, kann ihre Wiederherstellung mehr Schaden anrichten als ihr Ignorieren.

Die Identität gehört zum Speichervertrag

Gemeinsam genutzte Browser führen zu einer weiteren falschen Annahme, nämlich dass der Speicher dem Anwender gehört, der sich angemeldet hat. Tatsächlich gehört er zur Herkunft des Browsers.

Stellen Sie sich vor, eine Person melden sich ab und eine zweite melden sich auf demselben Gerät an. Verbleibende Entwürfe, Filter, Präferenzen oder im Cache gespeicherte Datensätze bleiben zurück, es sei denn, die App löscht sie oder ordnet sie in Namensräume ein. Allgemeine Schlüssel wie draft, recent-items oder dashboard-settings können die Daten der ersten Person in die Sitzung der zweiten Person übertragen. Selbst wenn nichts Sensibles involviert ist, wirkt dies wie eine Korruption, da einem falschen Benutzer ein gültiger Wert zugeordnet wurde.

Das Löschen aller Speicherinhalte beim Abmelden hilft zwar, ist aber unpräzise. Dadurch können legitime Geräte-Einstellungen wie Sprache oder Zugänglichkeitseinstellungen entfernt werden, und es kann zu Fehlern kommen, wenn das Abmelden in einem anderen Tab stattfindet, während der aktuelle Tab weiterhin ein In-Memory-Snapshot beibehält.

Ein überlegterer Entwurf ordnet die Daten in Kategorien ein, die sich nach dem Benutzer, der Sitzung, dem Gerät sowie öffentlichen Daten richten. Von einem Benutzer gehaltene Datensätze können die ID dieses Benutzers im Schlüssel oder in einem Wrapper-Objekt enthalten, und die Verifizierung kann sicherstellen, dass der gespeicherte Eigentümer mit der angemeldeten Identität übereinstimmt, bevor der Wert akzeptiert wird.

Entwürfe benötigen oft auch die ID des Dokuments oder Ressourcens. Die globale Wiederherstellung „des neuesten Entwurfs“ ist gefährlich, wenn mehrere Datensätze geöffnet sind, da Inhalte aus einem Datensatz in einem anderen auftauchen können, wenn der Schlüssel nicht angegeben hat, welches Objekt ihn besitzt. Namensräume dienen nicht nur der Ordnung – sie verhindern, dass gültige Daten in den falschen Kontext gelangen.

Verschlüsselung behebt keine Korruption

Bei Diskussionen über die Risiken der Browser-Speicherung wird oft auf die Sicherheit verwiesen. Sensible Informationen sollten nicht achtlos in der vom Browser zugänglichen Speicherung abgelegt werden, insbesondere nicht dort, wo eingeschobene Skripte darauf zugreifen können. Das ist zwar ein echtes Problem, doch es unterscheidet sich von dem Problem der Konsistenz.

Die Verschlüsselung eines gespeicherten Wertes hilft nicht dabei, zu verhindern, dass zwei Tabellen sich gegenseitig überschreiben. Sie führt weder zu Transaktionen noch zu Versionsprüfungen, Eigentumsregelungen oder Migrationsmechanismen. Eine Anwendung kann einen veralteten Snapshot fehlerfrei verschlüsseln und anschließend genauso fehlerfrei einen neueren Snapshot zerstören. Teams greifen manchmal auf Kodierung, Verfremdung oder Verschlüsselung zurück, um ein Problem zu lösen, das eigentlich ein Designproblem ist. Solche Maßnahmen können unter bestimmten Bedrohungsmodellen zur Vertraulichkeit beitragen, doch sie lassen den Update-Prozess genauso unzuverlässig.

Auch der Wechsel von localStorage zu IndexedDB kann mehr Speicherkapazität, strukturierten Speicher, Indexierung sowie transaktionale Funktionalitäten bieten. Allein führt das jedoch nicht zu einer korrekten Anwendungslogik. Eine Transaktion muss die richtigen Datensätze umfassen, Updates müssen weiterhin die Eigentumsverhältnisse berücksichtigen und Schema-Updates müssen weiterhin entworfen werden. Die Technologie bietet leistungsstärkere Primitiven, und die Anwendung muss diese unter einem klaren Konsistenzmodell nutzen.

Beginnen mit dem Eigentumskonzept

Anstatt zu überlegen, welche Speicheral API gewählt werden soll, sollte man zunächst klären, wer die Daten besitzt und wie Änderungen durch konkurrierende Schreiber behandelt werden sollen.

Einfache Präferenzen können oft nach dem Prinzip „Letzter Schreibvorgang gewinnt“ funktionieren. Wenn zwei Tabellen unterschiedliche Themen setzen, kann es genau das sein, was gewünscht wird, wenn das neueste Thema beibehalten wird – doch die Anwendung sollte weiterhin auf Änderungen in anderen Tabellen achten, falls die sichtbare Benutzeroberfläche sofort aktualisiert werden muss.

Ein unvollendeter Entwurf ist eine andere Sache. Ein „Letzter-Änderer-gewinnt“-Prinzip kann dem Benutzer echte Arbeit kosten, weshalb die Aufzeichnung möglicherweise eine Revisionnummer, ein Zeitstempel, eine Dokument-ID sowie eine Konfliktlösungspolitik benötigt. Bevor überschrieben wird, kann die App die Revisionen vergleichen, beide konkurrierenden Versionen beibehalten oder den Benutzer darauf hinweisen, dass eine andere Registerkarte denselben Entwurf bearbeitet hat.

Für wichtige Aufzeichnungen, die über verschiedene Geräte geteilt werden, sollte der Browser-Speicher überhaupt nicht als Quelle der Wahrheit dienen. Er kann als Offline-Cache oder vorübergehende Arbeitskopie fungieren, während der Server die autoritativen Versionen speichert und Konflikte löst.

Die richtige Gestaltung hängt davon ab, wie wertvoll die Daten sind und wie viele Benutzer sie bearbeiten. Wenn man eine Anzeigeeinstellung mit einem juristischen Formular, einem langen Artikelentwurf oder einer Offline-Finanzaufzeichnung vergleicht, sind die Risiken offensichtlich unterschiedlich. Sie alle als Zeichenketten unter willkürlichen Schlüsseln zu behandeln, ignoriert, wie teuer der Verlust einer Aktualisierung sein kann.

Ein einziger Schreibweg ist ebenfalls hilfreich. Wenn jedes Komponente jederzeit dieselbe Schlüsselwerte lesen, zusammenführen und ersetzen kann, kann niemand die Konsistenzregeln überprüfen. Ein Speichermodul kann Validierung, Migration, Synchronisierung und Konfliktbewältigung zentralisieren. Dafür ist keine umfangreiche Repository-Schicht für drei Einstellungen notwendig. So verhindert man, dass der Persistenzcode so unübersichtlich wird, dass niemand mehr sagen kann, welche Snapshots aktuell sind.

Erhalten Sie die Daten, interpretieren Sie sie nicht heimlich um

Die schlimmsten Speicherfehler melden sich nicht mit einem QuotaExceededError oder einem JSON-Parse-Fehler. Solche Fehler sind sichtbar und können protokolliert, getestet und behoben werden. Schädlich ist hingegen ein Schreibvorgang, der erfolgreich ist, während dabei die falsche Version ansonsten gültiger Daten gespeichert wird.

Stellen Sie sich ein veraltetes Tab vor, das ein Feld ändert und fünf alte Werte zurückbringt, einen verzögerten Callback, der einen neueren Entwurf überschreibt, eine alte Version, die ein veraltetes Schema speichert, oder ein neues Anmelden, das den Zustand des letzten Kontos übernimmt. Für die Storage-API scheint jede dieser Operationen erfolgreich gewesen zu sein. Die Korruption entsteht, weil die App niemals Versionen, Eigentumsrechte oder Konfliktregeln definiert hat.

Der Browser-Speicher findet seine Berechtigung, wenn seine Grenzen zur Aufgabe passen: Die Aufbewahrung von Präferenzen, das Caching nicht kritischer Werte, die Unterstützung von Offline-Workflows sowie die Wiederherstellung unvollendeter Arbeiten. Doch das Speichern von Inhalten verwandelt vorübergehenden Zustand in dauerhaften Eingabedaten, und das erfordert denselben Schutz wie Daten aus einem Server oder einer Datenbank.

In der Praxis bedeutet das, zu überprüfen, was zurückkommt, eine Versionierung durchzuführen, unverwandte Datensätze voneinander zu trennen, überall dort, wo das Eigentum relevant ist, Benutzer- und Ressourcenidentitäten hinzuzufügen, sowie keine riesigen Snapshots für kleine Änderungen neu zu schreiben. Es bedeutet auch, bewusst zu entscheiden, ob das Prinzip „Letzter Schreiber gewinnt“ akzeptabel ist, damit es nicht versehentlich zur Richtlinie wird.

Der Browser wird den letzten erhaltenen String getreulich beibehalten. Es liegt an Ihnen, sicherzustellen, dass der letzte Schreiber tatsächlich das Recht hatte, zu entscheiden, was erhalten bleibt.

Verwandte Artikel