Im Zuge der HTML5 Einführung sind einige interessante Technologien aufgekommen, die offlinefähige Anwendungen und Webseiten ermöglichen. Um die Unterschiede der verschiedenen Implementierungen und Browser beherrschbar zu machen, wurden über die Zeit diverse Bibliotheken, Wrapper, und Polyfills von Drittanbietern entwickelt. Dieser Artikel gibt einen kurzen Einblick in vergangene sowie aktuelle Technologien zur clientseitigen Speicherung von Daten und Zustand im Browser.

Cookies

Cookies dürften wohl der älteste – vermutlich sogar der bekannteste – Weg sein, um Zustand innerhalb des Browsers zu persistieren. HTTP Cookies werden in der Regel durch einen Web Server erzeugt und nach Aufruf einer ausgelieferten Webseite im Browser auf den zugreifenden Rechnern, Smartphones, und sonstigen Endgeräten gespeichert. Session Cookies können für eine Sitzung und Permanent Cookies können dauerhaft – beziehungsweise bis sie manuell entfernt werden – gültig sein.

Cookies werden für verschiedene Zwecke verwendet. Die Einsatzmöglichkeiten reichen von Authentifizierung, über Personalisierung, bis hin zur Nutzerverfolgung. Der gesamte Themenkomplex wirft datenschutzrechtliche Fragen auf, mit denen sich mittlerweile die europäische Gesetzgebung in Form der GDPR, sowie die kalifornische Gesetzgebung in Form der CPRA und CCPA befassen. Die Vorschriften für Deutschland hinsichtlich der Verwendung von Cookies und anderen Trackingtechnologien werden in der TTDSG reguliert.

Cookiebot ist eine datenschutzkonforme, allerdings kostenpflichtige, Komplettlösung zur Verwendung von Cookies. Mit Cookiebot können Anwendungen und Webseiten einen Dialog einblenden, in dem der Nutzung von Cookies zugestimmt oder widersprochen werden kann. Grundsätzlich gibt es aber auch Verwendungszwecke für Cookies die keiner Zustimmung und damit verbundener weiterer Dialoge bzw. Fenster bedürfen. Neben einer JavaScript SDK bietet Cookiebot mit der Plattform CMP regelmäßige Cookie audits sowie Berichte an. Weitere aktuelle Lösungen zur clientseitigen Verwaltung von Cookies sind z.B. js-cookie und ngx-cookie-service.

Cache

Browser-Cache exisiert ebenfalls seit der Mitte der 90er Jahre und ermöglicht es bereits abgerufene Ressourcen clientseitig zu speichern. Dadurch ist schnellerer Zugriff auf diese möglich, da sie nicht zwingend aus dem Internet nachgeladen werden müssen. Dieser Cache lässt sich serverseitig über die HTTP-Header konfigurieren und ist ohne weitere Programmierung im Client nutzbar. Der Application Cache, gelegentlich auch AppCache genannt, war Teil einer der ersten HTML5 Spezifikationen und hat es ermöglicht, Ressourcen einer Webseite lokal zu speichern. Dadurch konnte man auf diese im Nachhinein, selbst bei unterbrochener bzw. nicht bestehender Internetverbindung, erneut zugreifen. Hierzu wurden die zu lagernden Ressourcen in einer separaten *.appcache Datei definiert, die wiederrum per manifest-Attribut direkt in einem HTML-Dokument an der <html>-Wurzel referenziert werden konnte.

Beispiel eines Cache Manifests
CACHE MANIFEST
/main/home
/main/app.js
/settings/home
/settings/app.js

Durch Verwendung der „Speichern unter..“-Funktionalität der Browser konnten bereits seit den 90er Jahren Webseiten offline verfügbar gemacht werden. Mit dem Application Cache wurde dies erstmalig auch ohne weitere Aufwände für Besucher einer Webseite bzw. Webanwendung möglich. Application Cache ist mittlerweile veraltet und wird von aktuelleren Browsern nicht mehr unterstützt. In den folgenden Abschnitten werden wir uns mit weiteren Technologien auseinandersetzen, mit denen Zustand im Browser gespeichert oder aber Webseiten und Anwendungen offline zugänglich gemacht werden können.

Web Storage

Web Storage ist Teil der aktuellen HTML5 Spezifikation und beinhaltet sessionStorage und localStorage. Über den sessionStorage können Daten für einzelne Sitzungen gespeichert werden, die von jeder Unterseite einer Webseite im gleichen Browserfenster benutzt werden können. Mit dem localStorage können Daten auf eine ähnliche Weise gespeichert, allerdings auch über mehrere Sitzungen sowie über verschiedene Browserfenster hinweg verwendet werden. Daten können mit einem getter aus dem localStorage bezogen werden, allerdings ist hierbei etwas Vorsicht geboten, da kein locking-Mechanismus garantiert wird.

“The localStorage getter provides access to shared state. This specification does not define the interaction with other agent clusters in a multiprocess user agent, and authors are encouraged to assume that there is no locking mechanism. A site could, for instance, try to read the value of a key, increment its value, then write it back out, using the new value as a unique identifier for the session; if the site does this twice in two different browser windows at the same time, it might end up using the same „unique“ identifier for both sessions, with potentially disastrous effects.“

Benutzereinstellungen und -präferenzen wie zum Beispiel die Anzeigesprache, Schriftgröße, oder ein gewähltes Layout ließen sich beispielsweise gut im Web Storage speichern. Bereits gesuchte Begriffe, die innerhalb einer Suchleiste verwendet wurden, sind ein anderes sinnvolles Beispiel.

localStorage.setItem('languageSetting', 'de-DE');

const myLanguage = localStorage.getItem('languageSetting');

Übrigens können nur Strings im Web Storage gespeichert werden. Um beispielsweise ein Formular im Web Storage zwischenzuspeichern müssen dessen Inhalte zuerst einmal zu einem JSON-String konvertiert werden.

const formData = new Map([
  ['userName', userName],
  ['email', email]
]); // Illustratives Beispiel. Per new FormData(formElement) würde eine ähnliche Struktur entstehen.  
const formDataJSON = JSON.stringify(Object.fromEntries(formData))
sessionStorage.setItem('formData', formDataJSON)

Web SQL Datenbank

Zwischenzeitlich konnten solche Dinge auch in einer Web SQL Datenbank gespeichert werden. In einer früheren Version der HTML5 Spezifikation gab es die Überlegung eine Datenbank – WebSQL – im Browser einzuführen um Daten zu speichern, auf die mittels SQL-artiger queries zugegriffen werden kann. WebSQL wurde mittlerweile aus der Spezifikation entfernt und wird von der W3C Working Group nicht mehr aktiv gewartet. Stand heute wird Web SQL allerdings weiterhin von Chrome unterstützt.

Web SQL Beispiel
// Beispiel von Tutorials Point: https://www.tutorialspoint.com/html5/html5_web_sql.htm
var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); 

db.transaction(function (tx) { 
   tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)'); 
   tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")'); 
   tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")'); 
});

IndexedDB

IndexedDB hingegen wird auch heute noch von nahezu allen modernen Browsern unterstützt, wird aktiv gewartet, und ist heutzutage Teil des HTML5 Standards. Mit dieser Technologie können auch größere Mengen strukturierter Daten lokal gespeichert werden. Das lässt sich beispielsweise zum Caching, zum Herstellen von offlinefähigen Anwendungen, oder für Browsererweiterungen nutzen. Grundsätzlich wäre auch eine clientseitige Volltextsuche entwickelbar.

IndexedDB Beispiel
// Beispiel aus dem W3C Working Draft: https://www.w3.org/TR/IndexedDB/
const request = indexedDB.open("library");
let db;

request.onupgradeneeded = function() {
  // The database did not previously exist, so create object stores and indexes.
  const db = request.result;
  const store = db.createObjectStore("books", {keyPath: "isbn"});
  const titleIndex = store.createIndex("by_title", "title", {unique: true});
  const authorIndex = store.createIndex("by_author", "author");

  // Populate with initial data.
  store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
  store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
  store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});
};

request.onsuccess = function() {
  db = request.result;
};

Um in Browsern die WebSQL und IndexedDB anbieten, die Entscheidung der verwendeten Technologie abzugeben, kann localForage verwendet werden. Das ist eine Bibliothek, die als Wrapper für Web SQL, IndexedDB, und localStorage fungiert und für den Zugriff eine einheitliche Schnittstelle bietet. In Browsern ohne Web SQL oder IndexedDB greift sie als Fallback auf localStorage zu.

PouchDB

Auch wenn offlinefähige Anwendungen praktisch klingen, haben sich dadurch initial eine Reihe neuer Komplexitäten – wie zum Beispiel die Synchronisierung des Datenbestandes von Client und Server – ergeben. PouchDB ist eine quelloffene Datenbanktechnologie, die das Erstellen offlinefähiger Anwendungen erleichtert. Mit PouchDB ist es möglich Daten lokal zu speichern, die im Nachhinein mit einer CouchDB und kompatiblen Servern synchronisiert werden können, sobald wieder eine Internetverbindung besteht.

PouchDB Beispiel
//Beispiel von PouchDB: https://pouchdb.com/
var db = new PouchDB('dbname');

db.put({
  _id: '[email protected]',
  name: 'David',
  age: 69
});

db.changes().on('change', function() {
  console.log('Ch-Ch-Changes');
});

db.replicate.to('http://example.com/mydb');

RxDB

RxDB ist eine Technologie, die dem Offline-first-Ansatz folgt und Replikation mittels CouchDB oder GraphQL Endpunkten unterstützt. Neben einer quelloffenen JavaScript Bibliothek bietet RxDB auch ein kostenpflichtiges Premium Paket an, mit dem eine Integration zu weiteren Systemen wie OPFS, IndexedDB und SQLite möglich ist. Ferner beinhaltet die Premiumvariante weitere Plugins die unter anderem bei der Migration, Query Optimierung, und Verschlüsselung hilfreich sein können.

Ein alternativer Ansatz zu den bisher erwähnten Bibliotheken, um Synchronisierung oder Datenreplikation durchzuführen, ist z.B. automerge.

Service Worker

Service Worker sind virtuelle Proxies zwischen dem Client und dem Netzwerk und werden mittlerweile von nahezu allen Browsern nativ unterstützt. Sie können nur innerhalb eines sicheren Kontextes (HTTPS) verwendet werden und ermöglichen das Entwickeln von Offline-first-Anwendungen. Service Worker an sich sind jedoch keine Speichertechnologie. Als virtueller Proxy leben Service Worker auf der Clientseite innerhalb des Browsers und operieren „irgendwo zwischen“ dem Client und dem Server. Funktional betrachtet können mittels Service Worker innerhalb des Clients einzelne HTTP-Requests abgefangen werden, und bieten dem Client dadurch ein Zeitfenster in dem noch etwas anderes gemacht werden kann, bevor der Request an den Server gesendet wird. Beispielsweise kann innerhalb dieses Zeitfensters überprüft werden, ob eine Internetverbindung besteht, um im Zweifelsfall lokal vorhandene Daten und Ressourcen zu verwenden und die HTTP-Requests abzubrechen. Technisch gesehen können mit diesem Standard auch andere Prozesse als Speicherung bzw. Abruf lokal vorhandener Daten durchgeführt werden, die nicht zwingend gutartiger Natur sein müssen, was sicher auch eine Erklärung für die HTTPS Anforderung bietet.

Registrierung eines Service Worker
//Beispiel von https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#registering_your_worker
const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/",
      });
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

// …

registerServiceWorker();

Mittels der Cache API lassen sich HTTP-Responses lokal speichern, wodurch in Kombination mit Service Worker Ressourcen bereits browsernativ speicher- und verwendbar sind.

Speichern von Ressourcen per Cache API
//Beispiel von https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#install_and_activate_populating_your_cache
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ])
  );
});

State Management

Durch die Einführung bzw. Verbreitung von Single Page Applications (SPA) wurde die Verwaltung von größeren Mengen von Zustand auf der Clientseite notwendig. Im Zuge dessen sind eine Reihe von Bibliotheken entstanden, die die Zustandsverwaltung an einer zentralen Stelle ermöglichen und damit eine saubere Trennung von UI und Datenhaltung innerhalb des Clients gewährleisten. Ein Beispiel dafür ist das von Facebook übernommene refluxjs, das mit der sogenannten Flux Architektur bereits ein Architekturmuster vorgegeben und dadurch die Entwicklung von React Anwendungen vereinfacht hat. Reflux wird allerdings mittlerweile nicht mehr aktiv gewartet. Das Flux Team empfiehlt als Alternativen für React Anwendungen Recoil, Zustand, Jotai, Redux, oder MobX.

Redux folgt einem unidirektionalen Ansatz in Bezug auf den Datenfluss und ist speziell für die Benutzung mit React entwickelt worden. Grundsätzlich lässt sich Redux aber auch ohne React verwenden. Zum Beispiel kann mit Redux und Angular auch eine offlinefähige Anwendung gebaut werden. VueX ist das Pendant zu Redux in der Vue.js Welt und verfolgt ebenfalls einen unidirektionalen Ansatz.

MobX ist ein flexiblerer Weg zur Zustandsverwaltung als die bisherigen Beispiele. Obwohl MobX vorgibt „unopinionated“ zu sein, müssen dennoch einige Vorgaben eingehalten werden. Beispielsweise hilft es bereits Kenntnisse über Observer und Decorators zu haben, um mit MobX effektiv arbeiten zu können, jedoch werden in dieser Bibliothek tatsächlich keine größeren Architekturvorgaben getroffen. Hier ist ein kleines editierbares Beispiel das zeigt, wie mit React und MobX ein zurücksetzbarer Zeitmesser erstellt werden kann.

Für gewöhnlich wird Zustand von diesen Bibliotheken in der JavaScript Laufzeitumgebung (in-memory) gespeichert, allerdings kann bei Bedarf z.B. middleware in Redux dazu benutzt werden um Daten bzw. Zustand mittels beispielsweise des localStorage über mehrere Sitzungen hinweg zu persistieren.

Relikte

Neben den bereits erwähnten Standards und Bibliotheken sind im Verlauf der letzten 12 Jahre diverse Polyfills, Wrapper, und Bibliotheken entstanden die mittlerweile zwar nicht mehr gewartet werden, gegebenenfalls aber für Webanwendungen und -seiten, die unbedingt in veralteten Browsern laufen müssen, interessant sein könnten.

Beispiele dafür sind:

Fazit

Im Laufe der Zeit sind diverse experimentelle Technologien und Bibliotheken entstanden und verschwunden. Mit Web Storage, IndexedDB und CacheAPI scheinen sich mittlerweile einige Standards neben Cookies etabliert zu haben, die in Zukunft zum Speichern von Daten verwendet werden können. Durch Verwendung von Service Worker oder Offline-first-Bibliotheken können Anwendungen gebaut werden die selbst in Regionen mit unvollständiger Netzabdeckung zuverlässig verwendet werden können. Um eine konsistente Datenbasis zu gewährleisten können Bibliotheken zur Synchronisierung oder CRDT verwendet werden.

Durch geschicktes Vorgehen in der Entwicklung von Anwendungen kann die Performanz auf Client- als auch auf Serverseite verbessert werden, da durch kluges Vorhalten von Daten auf dem Client gegebenenfalls weniger Netzwerkanfragen nötig und dadurch Serverlasten reduziert werden können.

Mit State Management Bibliotheken kann Zustand an einer zentralen Stelle verwaltet werden, wodurch die Entwicklung von SPA erleichtert werden kann. Zustand wird hierbei für gewöhnlich nur temporär gespeichert, kann allerdings durch Kombination mit weiteren Technologien auch dauerhaft persistiert werden. Die Komplexität auf Detailebene wird zwar leichter beherrschbar, kann allerdings durch die Einführung vieler verschiedener Technologien und Bibliotheken auf der Makroebene auch steigen. Außerdem sollte immer beachtet werden, dass im Browser gespeicherte Daten verloren gehen können – beispielsweise, wenn der Browsercache geleert wird. Letzteres ließe sich verhindern, in dem die Webanwendung per Cordova oder ähnlichen Technologien verpackt und ausgeliefert werden würde. Um Datenverlust durch Geräteverlust oder -schaden zu vermeiden, sollte gegebenenfalls Datenreplikation auf einem Server durchgeführt werden. Idealerweise sollten vor der Verwendung irgendwelcher Technologien oder Bibliotheken die Anforderungen ordentlich geprüft sowie die Vor- und Nachteile abgewogen werden.


Vielen Dank für das Feedback an Piet, Joachim, Martina - und vor allem Christiane, insbesondere für ihre Unterstützung in der Strukturierung dieses Artikels.