JS
() =>
JavaScript · IndexedDB · Offline-first · PWA · Browser Storage
JavaScript IndexedDB
Offline-Datenbank im Browser richtig nutzen

IndexedDB ist die mächtigste Offline-Speicherlösung im Browser: strukturierte Daten, Gigabyte-Kapazität, Transaktionen, Indexes und asynchrone Abfragen ohne Block des UI-Threads. Wer die Grundlagen beherrscht, baut Offline-first-Apps, die auch ohne Netzwerkverbindung vollständig funktionieren.

18 Min. Lesezeit Transaktionen · Indexes · Cursor · Sync · Service Worker Browser · PWA · Offline-first · Dexie.js

1. Warum IndexedDB statt localStorage und sessionStorage?

IndexedDB und localStorage lösen grundlegend verschiedene Probleme. localStorage ist synchron und blockiert den Haupt-Thread bei jedem Lese- und Schreibvorgang. Es akzeptiert nur Strings, hat eine harte Grenze von 5–10 MB je nach Browser und unterstützt keine strukturierten Abfragen. Für einfache Präferenzen und kleine Datenmengen ist das ausreichend – für Offline-first-Apps, Caches, Nutzerdaten und größere Datensätze ist es das falsche Werkzeug.

IndexedDB ist vollständig asynchron – alle Operationen laufen in Hintergrund-Threads und blockieren nie den UI-Thread. Es akzeptiert JavaScript-Objekte direkt, ohne JSON-Serialisierung, und unterstützt Arrays, Blobs, Dates und verschachtelte Strukturen. Die Speicherkapazität ist enorm: in modernen Browsern typischerweise 50 % des verfügbaren Gerätespeichers oder mehr. Mit Indexes und Cursors lassen sich gezielte Abfragen, Sortierungen und Bereichsabfragen durchführen – Funktionalität, die localStorage vollständig fehlt. Für Progressive Web Apps, Dokumenteneditoren, E-Mail-Clients und jede Anwendung, die offline funktionieren soll, ist IndexedDB die einzige ernsthafte Option.

2. Datenbankstruktur: Object Stores, Keys und Versionierung

IndexedDB organisiert Daten in Object Stores – vergleichbar mit Tabellen in relationalen Datenbanken, aber ohne festes Schema. Jeder Object Store hat einen Key Path oder einen Auto-Increment-Key. Der Key Path definiert, welche Eigenschaft des gespeicherten Objekts als Schlüssel verwendet wird – zum Beispiel id oder email. Auto-Increment erzeugt automatisch aufsteigende numerische Keys, ähnlich wie SERIAL in PostgreSQL.

Das Datenbankschema wird versioniert. Wenn eine neue Version der App ein neues Schema benötigt – einen neuen Object Store, einen neuen Index, das Löschen eines veralteten Stores – wird die Versionsnummer erhöht. Der Browser ruft dann den onupgradeneeded-Handler auf, bevor andere Transaktionen beginnen können. Das ist der einzige Moment, in dem das Schema geändert werden kann. Migrationen in IndexedDB sind damit deklarativ: jede Versionsnummer entspricht einem bestimmten Schema-Zustand, und der Upgrade-Handler bringt den Store schrittweise auf den neuesten Stand. Das ist wichtig für Apps, die mehrere Versionen des Schemas auf verschiedenen Geräten gleichzeitig haben können.


// Open IndexedDB with schema migration support
const DB_NAME = 'mironsoft-app';
const DB_VERSION = 3;

/**
 * Opens the database and handles schema migrations.
 * @returns {Promise<IDBDatabase>}
 */
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      const oldVersion = event.oldVersion;

      // Incremental migrations — each version builds on the previous
      if (oldVersion < 1) {
        // Version 1: basic user store with email index
        const users = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
        users.createIndex('by_email', 'email', { unique: true });
      }

      if (oldVersion < 2) {
        // Version 2: add orders store with compound index
        const orders = db.createObjectStore('orders', { keyPath: 'id' });
        orders.createIndex('by_user_status', ['userId', 'status'], { unique: false });
        orders.createIndex('by_created_at', 'createdAt', { unique: false });
      }

      if (oldVersion < 3) {
        // Version 3: add sync queue for offline operations
        const syncQueue = db.createObjectStore('sync_queue', {
          keyPath: 'queueId',
          autoIncrement: true,
        });
        syncQueue.createIndex('by_entity', 'entity', { unique: false });
      }
    };

    request.onblocked = () => {
      // Another tab has an older version open — ask user to reload
      console.warn('IndexedDB upgrade blocked — please close other tabs');
    };
  });
}

4. Transaktionen: ACID-Eigenschaften im Browser

IndexedDB-Transaktionen sind ACID-konform: atomar, konsistent, isoliert und dauerhaft. Eine Transaktion umfasst einen oder mehrere Object Stores und hat eine definierte Zugriffsart: readonly für Lesezugriff und readwrite für Lese- und Schreibzugriff. Readonly-Transaktionen können parallel ausgeführt werden; Readwrite-Transaktionen auf demselben Store werden serialisiert. Transactions commiten automatisch, wenn der letzte Request abgeschlossen ist und keine neuen Requests mehr starten – oder sie rollen bei einem Fehler automatisch zurück.

Ein häufiger Fehler: eine Transaktion über einen asynchronen Aufruf hinweg offen halten. Wenn man nach einem await fetch() einen Schreibvorgang auf einer bereits commited Transaktion versucht, erhält man einen TransactionInactiveError. Der Grund: IndexedDB-Transaktionen haben einen Ereignis-Loop-Slot – sobald alle synchronen Callbacks in einem Slot abgearbeitet sind und die Kontrolle an den Ereignis-Loop zurückgegeben wird, wird die Transaktion commited. Netzwerk-Requests übergeben die Kontrolle an den Ereignis-Loop. Die Lösung: alle Daten, die man schreiben möchte, vor dem Start der Transaktion vollständig vorbereiten.

5. Indexes: schnelle Abfragen ohne Full-Scan

Ohne Indexes kann IndexedDB Objekte nur per Primary Key abrufen. Für alle anderen Abfragen – nach E-Mail, Status, Datum, Kategorie – müsste man alle Datensätze laden und filtern: ein Full-Scan. Indexes in IndexedDB beschleunigen Abfragen auf beliebigen Eigenschaften oder Eigenschaftskombinationen erheblich. Ein Index auf email ermöglicht O(log n) Lookups per E-Mail. Ein zusammengesetzter Index auf [userId, status] ermöglicht effiziente Abfragen wie "alle Bestellungen von User 42 mit Status 'pending'".

Abfragen mit Indexes nutzen das IDBKeyRange-Objekt, das Bereichsabfragen ermöglicht: IDBKeyRange.only(value) für exakte Treffer, IDBKeyRange.lowerBound(value) für alle Einträge größer als ein Wert, IDBKeyRange.bound(lower, upper) für Bereichsabfragen. Diese Ranges kombiniert mit einem Index ergeben SQL-ähnliche WHERE-Klauseln ohne SQL-Engine. Wichtig: Indexes werden immer in onupgradeneeded angelegt, nie zur Laufzeit. Das Anlegen eines Indexes auf einem gefüllten Store kann bei großen Datensätzen Zeit brauchen – Browser zeigen in dieser Phase oft einen "Wird aktualisiert"-Dialog.


// CRUD operations with proper transaction handling
class OrderRepository {
  #db;

  constructor(db) {
    this.#db = db;
  }

  /**
   * Save or update an order using a readwrite transaction.
   * @param {object} order
   * @returns {Promise<IDBValidKey>}
   */
  save(order) {
    return new Promise((resolve, reject) => {
      const tx = this.#db.transaction('orders', 'readwrite');
      const store = tx.objectStore('orders');

      // Enrich before transaction — never await network calls inside a transaction
      const enriched = { ...order, updatedAt: new Date().toISOString() };
      const req = store.put(enriched);

      req.onsuccess = () => resolve(req.result);
      tx.onerror = () => reject(tx.error);
    });
  }

  /**
   * Query orders by userId and status using a compound index.
   * @param {number} userId
   * @param {string} status
   * @returns {Promise<object[]>}
   */
  findByUserAndStatus(userId, status) {
    return new Promise((resolve, reject) => {
      const tx = this.#db.transaction('orders', 'readonly');
      const index = tx.objectStore('orders').index('by_user_status');

      // IDBKeyRange for compound index — exact match on [userId, status]
      const range = IDBKeyRange.only([userId, status]);
      const req = index.getAll(range);

      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }

  /**
   * Delete an order by primary key.
   */
  delete(id) {
    return new Promise((resolve, reject) => {
      const tx = this.#db.transaction('orders', 'readwrite');
      const req = tx.objectStore('orders').delete(id);
      req.onsuccess = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }
}

6. Cursor: große Datenmengen effizient durchlaufen

Wenn man alle Datensätze eines Object Stores oder Indexes verarbeiten möchte, ohne sie alle auf einmal in den Speicher zu laden, sind Cursor das richtige Werkzeug. Ein IndexedDB-Cursor zeigt auf einen einzelnen Datensatz und wandert per cursor.continue() zum nächsten. Das lädt immer nur einen Datensatz gleichzeitig – O(1) Speicherbedarf für beliebig große Stores. Cursor können auch Datensätze beim Durchlaufen löschen oder aktualisieren, was Batch-Updates ermöglicht, ohne alle IDs vorab zu laden.

Cursor können mit einer Richtung (next, prev) und einem IDBKeyRange kombiniert werden, um durch einen Teilbereich des Stores oder Indexes zu navigieren. Das erlaubt paginierte Abfragen in IndexedDB: den Cursor auf den Start der Seite setzen, eine definierte Anzahl Datensätze lesen, beim nächsten Seitenaufruf den letzten gelesenen Key als Cursor-Startpunkt verwenden. Keyset-Pagination ist für IndexedDB erheblich effizienter als Offset-Pagination, weil kein Full-Scan bis zum Offset nötig ist.

7. Promise-Wrapper für ergonomische IndexedDB-Nutzung

Die native IndexedDB-API ist Event-basiert: jede Operation ist ein IDBRequest mit onsuccess- und onerror-Callbacks. Das führt zu verschachteltem, schwer lesbarem Code. Die ergonomische Lösung: einen dünnen Promise-Wrapper schreiben, der jede Operation in ein Promise kapselt, oder eine Bibliothek wie Dexie.js verwenden, die eine vollständige, sauber typisierte Promise-API über IndexedDB legt.

Dexie.js ist die populärste Abstraktion für IndexedDB: sie bietet deklaratives Schema-Management, Promise-basierte CRUD-Operationen, umfangreiche Abfrage-API mit .where(), .equals(), .between() und vollständiges TypeScript-Support. Die Abstraktion ist dünn genug, dass man bei Bedarf auf die native IndexedDB-API zurückfallen kann. Für die meisten Projekte ist Dexie die beste Balance zwischen Ergonomie und Flexibilität – ohne den Overhead schwergewichtiger ORM-Bibliotheken.


// Dexie.js — ergonomic IndexedDB wrapper with typed schema
import Dexie from 'dexie';

// Define schema with TypeScript-style type hints in comments
const db = new Dexie('mironsoft-app');

db.version(3).stores({
  users: '++id, &email, name',        // ++ = autoincrement, & = unique index
  orders: 'id, [userId+status], createdAt',
  sync_queue: '++queueId, entity',
});

// Typed helper — clean async/await API
async function getUserByEmail(email) {
  return db.users.where('email').equals(email).first();
}

async function getPendingOrdersForUser(userId) {
  return db.orders
    .where('[userId+status]')
    .equals([userId, 'pending'])
    .sortBy('createdAt');
}

// Batch import with transaction — atomic: all or nothing
async function importOrders(orders) {
  return db.transaction('rw', db.orders, async () => {
    for (const order of orders) {
      await db.orders.put({
        ...order,
        syncedAt: new Date().toISOString(),
      });
    }
  });
}

// Pagination with Dexie — keyset pagination via offset
async function getOrderPage(pageSize, offset) {
  return db.orders
    .orderBy('createdAt')
    .reverse()
    .offset(offset)
    .limit(pageSize)
    .toArray();
}

8. Offline-Sync-Strategien mit Service Worker

Die vollständige Stärke von IndexedDB entfaltet sich in Kombination mit dem Service Worker für Offline-first-Apps. Das Grundprinzip: wenn der Nutzer offline ist, werden Schreiboperationen nicht direkt an die API gesendet, sondern als ausstehende Operationen in einer Sync-Queue in IndexedDB gespeichert. Sobald Netzwerkverbindung besteht, verarbeitet der Service Worker die Queue und synchronisiert die Daten mit dem Server. Der Service Worker kann dafür die Background Sync API nutzen, die das Synchronisieren auch dann ermöglicht, wenn die App nicht geöffnet ist.

Konfliktbehandlung ist die schwierigste Herausforderung bei Offline-Sync. Wenn derselbe Datensatz offline auf zwei Geräten geändert wurde, muss eine Merge-Strategie entschieden werden: Last-Write-Wins (einfach, aber verlustbehaftet), Three-Way-Merge (komplex, aber vollständig) oder versionsvektorbasiertes Merging (für verteilte Systeme). Für die meisten Apps ist eine serverbasierte Konfliktlösung mit Timestamps praktikabel: der Client schickt den letzten bekannten Server-Timestamp mit; wenn der Server eine neuere Version hat, meldet er den Konflikt zurück und der Client kann den Nutzer um Auflösung bitten oder automatisch mergen.

9. Browser-Storage-Optionen im Vergleich

Die Wahl der richtigen Browser-Storage-Technologie hängt von der Datenmenge, der Struktur und dem Abfragebedarf ab. IndexedDB ist die richtige Wahl für strukturierte, abfragbare Daten in großen Mengen – aber für einfache Anwendungsfälle ist sie Overkill.

Storage-Option Kapazität API-Typ Abfragen Ideal für
localStorage 5–10 MB Synchron Nur Key/Value Einstellungen, Tokens
sessionStorage 5–10 MB Synchron Nur Key/Value Tab-spezifische Daten
Cache API GB-Bereich Async / Promise Request/Response-Paar HTTP-Antworten, Assets
IndexedDB GB-Bereich Async / Event Indexes, Ranges, Cursor Strukturierte Daten, Offline-Apps
OPFS GB-Bereich Async / Sync (Worker) Dateisystem SQLite-WASM, Dateien

Ein neuerer Ansatz: SQLite als WASM-Modul mit dem Origin Private File System (OPFS) als Storage-Backend. Das ermöglicht echtes SQL in der App – inklusive JOINs, komplexer Aggregation und vollständiger Transaktionssemantik. IndexedDB bleibt aber die plattstformübergreifend stabilste Wahl ohne WebAssembly-Abhängigkeit. Für Apps, die maximale SQL-Kompatibilität brauchen, ist SQLite-WASM eine interessante Alternative; für die meisten Offline-first-Apps reicht IndexedDB mit Dexie.js vollständig aus.

Mironsoft

Progressive Web Apps, Offline-first-Architektur und Browser-Storage

Offline-first-App für Ihre Nutzer?

Wir entwickeln PWAs mit IndexedDB-basierter Offline-Datenhaltung, Service-Worker-Sync und Konfliktauflösung – damit Ihre App auch ohne Netzwerkverbindung vollständig funktioniert.

PWA-Entwicklung

IndexedDB-Datenhaltung, Service Worker und Background Sync für vollständige Offline-Fähigkeit

Sync-Architektur

Bidirektionale Synchronisation zwischen IndexedDB und Backend-API mit Konfliktauflösung

Storage-Migration

Migration von localStorage-basierten Apps zu IndexedDB mit versioniertem Schema und Dexie.js

10. Zusammenfassung

IndexedDB ist die Offline-Datenbank des Browsers: asynchron, strukturiert, gigabytes-fähig und mit vollständiger Transaktionssemantik. Die wichtigsten Konzepte: Object Stores für die Datenhaltung, versioniertes Schema-Management über onupgradeneeded, Transaktionen mit ACID-Garantien, Indexes für O(log n) Abfragen und Cursor für speichereffiziente Traversierung großer Datensätze. Die event-basierte native API wird durch Promise-Wrapper oder Dexie.js auf ein modernes async/await-Niveau gehoben.

In Kombination mit dem Service Worker und der Background Sync API wird IndexedDB zum Herzstück von Offline-first-Apps. Schreiboperationen landen in einer Sync-Queue, werden lokal sofort angewendet und bei Netzwerkverbindung automatisch synchronisiert. Das gibt Nutzern eine reaktive App-Erfahrung unabhängig von der Netzwerkqualität. Für Projekte, die SQL-Abfragepower brauchen, ist SQLite-WASM über OPFS eine aufkommende Alternative – für die Mehrheit der Offline-first-Anwendungsfälle ist IndexedDB mit Dexie.js die pragmatischste und robusteste Lösung.

IndexedDB — Das Wichtigste auf einen Blick

Schema & Versionierung

Schema-Änderungen nur in onupgradeneeded. Inkrementelle Migrationen pro Versionsnummer – wichtig für Apps auf verschiedenen Geräten mit verschiedenen Schema-Versionen.

Transaktionen

Nie await auf Netzwerk-Requests innerhalb einer Transaktion. Alle Daten vor dem Transaktionsstart vorbereiten – Transaktion commitet automatisch am Ende des Ereignis-Loop-Slots.

Indexes & Abfragen

Indexes für alle Abfragen außer Primary Key anlegen. IDBKeyRange für Bereichsabfragen. Zusammengesetzte Indexes für kombinierte Filterung – kein Full-Scan nötig.

Dexie.js & Offline-Sync

Dexie.js als ergonomische Abstraktion. Sync-Queue in IndexedDB für Offline-Operationen. Service Worker + Background Sync für automatische Synchronisation bei Netzwiederkehr.

11. FAQ: JavaScript IndexedDB

1Wie viel Speicher kann IndexedDB nutzen?
Typischerweise 50 % des freien Gerätespeichers – mehrere Gigabytes. Mit navigator.storage.persist() um dauerhaften Speicher bitten, der nicht automatisch gelöscht wird.
2Warum ist IndexedDB so komplex?
Asynchron + transaktionsbasiert = mehr Boilerplate. Dexie.js reduziert die Komplexität auf ein modernes async/await-Niveau. Die Komplexität bringt Gigabyte-Kapazität und ACID-Transaktionen.
3Daten beim Cache-Löschen weg?
Ja, bei "Alle Zeit" löschen. Mit navigator.storage.persist() dauerhaften Speicher beantragen – vor automatischem Browser-Aufräumen geschützt.
4IndexedDB im Service Worker?
Ja – vollständig zugänglich. Service Worker liest Sync-Queue aus IndexedDB und sendet ausstehende Operationen bei Netzwerkverbindung automatisch ab.
5IndexedDB vs. Cache API?
Cache API: HTTP-Antworten und Assets cachen. IndexedDB: strukturierte Anwendungsdaten mit Abfragemöglichkeiten. Beide ergänzen sich in Offline-first-Apps.
6Schema-Migrationen in IndexedDB?
In onupgradeneeded mit if (oldVersion < N)-Checks. Inkrementelle Migrationen – jede Version baut auf der vorherigen auf. Schema-Änderungen nur hier möglich.
7Kein await auf Netzwerk in Transaktionen?
await fetch() übergibt Kontrolle an den Ereignis-Loop – Transaktion commitiert automatisch. Danach: TransactionInactiveError. Alle Daten vor der Transaktion vorbereiten.
8Wofür sind Cursor gut?
O(1) Speicher für beliebig große Stores – immer nur ein Datensatz gleichzeitig. Ideal für Batch-Exports, inkrementelle Verarbeitung und Keyset-Pagination.
9Dexie.js oder native IndexedDB-API?
Dexie.js für die meisten Projekte: Promise-API, TypeScript-Support, elegante Abfragesyntax. Native API nur für minimale Abhängigkeiten oder spezifische Transaktionssteuerung.
10Offline-Sync-Konflikte lösen?
Timestamp-basiertes Last-Write-Wins mit Server-Hoheit als pragmatische Lösung. Client schickt letzten bekannten Timestamp – Server meldet Konflikte zurück.