Skip to content

Spielstand mit IndexedDB speichern und laden

IndexedDB ist die beste Option für dauerhafte Spieldaten im Browser. Es kommt mit großen Datenmengen zurecht, funktioniert offline und blockiert den Haupt-Thread nicht.

1) Warum IndexedDB statt localStorage?

MerkmallocalStorageIndexedDB
Speichergrenze~5-10 MB50+ MB (oft GB)
Datentypennur StringsObjekte, Blobs, Arrays
Asynchronnein (blockiert)ja
Indizierte Abfragenneinja

Für Spiele ist IndexedDB fast immer die richtige Wahl.

2) Eine Datenbank öffnen

js
function openGameDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('MyGame', 1)
    
    request.onerror = () => reject(request.error)
    request.onsuccess = () => resolve(request.result)
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result
      
      // Create stores
      if (!db.objectStoreNames.contains('saves')) {
        db.createObjectStore('saves', { keyPath: 'slot' })
      }
      if (!db.objectStoreNames.contains('settings')) {
        db.createObjectStore('settings', { keyPath: 'key' })
      }
    }
  })
}

3) Spielstand speichern

js
async function saveGame(slot, gameState) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const saveData = {
      slot,
      state: gameState,
      timestamp: Date.now(),
    }
    
    const request = store.put(saveData)
    request.onsuccess = () => resolve()
    request.onerror = () => reject(request.error)
  })
}

4) Spielstand laden

js
async function loadGame(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.get(slot)
    request.onsuccess = () => resolve(request.result?.state || null)
    request.onerror = () => reject(request.error)
  })
}

5) Alle Spielstände auflisten

js
async function listSaves() {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.getAll()
    request.onsuccess = () => resolve(request.result)
    request.onerror = () => reject(request.error)
  })
}

6) Einen Spielstand löschen

js
async function deleteSave(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const request = store.delete(slot)
    request.onsuccess = () => resolve()
    request.onerror = () => reject(request.error)
  })
}

7) Eine vollständige GameStorage-Klasse

js
class GameStorage {
  constructor(dbName = 'GameData', version = 1) {
    this.dbName = dbName
    this.version = version
    this.db = null
  }
  
  async init() {
    this.db = await this.openDB()
  }
  
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)
      request.onerror = () => reject(request.error)
      request.onsuccess = () => resolve(request.result)
      request.onupgradeneeded = (e) => {
        const db = e.target.result
        if (!db.objectStoreNames.contains('saves')) {
          db.createObjectStore('saves', { keyPath: 'slot' })
        }
        if (!db.objectStoreNames.contains('settings')) {
          db.createObjectStore('settings', { keyPath: 'key' })
        }
        if (!db.objectStoreNames.contains('assets')) {
          db.createObjectStore('assets', { keyPath: 'url' })
        }
      }
    })
  }
  
  async save(slot, data) {
    const tx = this.db.transaction('saves', 'readwrite')
    tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
    return tx.complete
  }
  
  async load(slot) {
    const tx = this.db.transaction('saves', 'readonly')
    const result = await this.promisify(tx.objectStore('saves').get(slot))
    return result?.data || null
  }
  
  async setSetting(key, value) {
    const tx = this.db.transaction('settings', 'readwrite')
    tx.objectStore('settings').put({ key, value })
  }
  
  async getSetting(key, defaultValue = null) {
    const tx = this.db.transaction('settings', 'readonly')
    const result = await this.promisify(tx.objectStore('settings').get(key))
    return result?.value ?? defaultValue
  }
  
  promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }
}

8) Binärdaten ablegen (Texturen, Audio)

IndexedDB kommt mit Blobs und ArrayBuffers zurecht:

js
async function cacheAsset(url, blob) {
  const tx = db.transaction('assets', 'readwrite')
  tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}

async function getCachedAsset(url) {
  const tx = db.transaction('assets', 'readonly')
  const result = await promisify(tx.objectStore('assets').get(url))
  return result?.blob || null
}

9) Muster für Autosave

js
class AutoSave {
  constructor(storage, interval = 60000) {
    this.storage = storage
    this.interval = interval
    this.timer = null
    this.dirty = false
  }
  
  markDirty() {
    this.dirty = true
  }
  
  start(getState) {
    this.timer = setInterval(async () => {
      if (this.dirty) {
        await this.storage.save('autosave', getState())
        this.dirty = false
        console.log('Autosaved')
      }
    }, this.interval)
  }
  
  stop() {
    clearInterval(this.timer)
  }
}

10) Fehlerbehandlung und Rückfallwege

js
async function safeLoad(slot, defaultState) {
  try {
    const saved = await loadGame(slot)
    if (saved) {
      // Validate/migrate old saves if needed
      return migrateSave(saved)
    }
  } catch (err) {
    console.warn('Failed to load save:', err)
  }
  return defaultState
}

function migrateSave(save) {
  // Handle old save formats
  if (!save.version) {
    save.version = 1
    save.settings = save.settings || {}
  }
  return save
}

11) Spielstände vor dem Verdrängen schützen

IndexedDB bleibt nicht garantiert erhalten. Standardmäßig nutzt eine Origin "Best-Effort"-Speicher, und der Browser darf ihn räumen, wenn die Platte volläuft oder, unter Safari/WebKit, nach längerer Zeit ohne Nutzerinteraktion mit deiner Seite. Bei Spielständen sind das genau die Daten, die du nicht verlieren willst.

Fordere persistenten Speicher an, damit der Browser deine Daten nicht ohne ausdrückliche Nutzeraktion löscht:

js
async function makeStoragePersistent() {
  if (navigator.storage && navigator.storage.persist) {
    const persisted = await navigator.storage.persist()
    console.log(persisted ? 'Saves are protected from eviction' : 'Saves may be evicted under storage pressure')
    return persisted
  }
  return false
}

Browser entscheiden anhand von Interaktionssignalen, ob sie das gewähren (wie stark der Nutzer mit deiner Seite interagiert, ob sie als PWA installiert ist), nimm also nicht an, dass es immer klappt. Du kannst auch prüfen, wie viel Platz du hast, bevor du große Spielstände oder gecachte Assets schreibst:

js
async function checkStorage() {
  if (navigator.storage && navigator.storage.estimate) {
    const { usage, quota } = await navigator.storage.estimate()
    console.log(`Using ${usage} of ${quota} bytes`)
  }
}

Beide APIs brauchen einen sicheren Kontext (HTTPS oder localhost).

Verwandt

Externe Ressourcen