If you have ever built a browser game that relies on localStorage for saving high scores, you have probably experienced that sinking feeling when a player clears their cache and wipes out three weeks of grinding. Worse yet, localStorage is synchronous, blocks the main thread, and has a measly 5 MB storage limit that shatters the moment your indie puzzle game starts caching procedurally generated textures or compressed audio stems.
Enter IndexedDB—the unsung hero of Progressive Web App (PWA) game development.
Whether you are optimising a retro platformer for mobile browsers or porting a sprawling idle clicker to modern web runtimes, mastering client-side persistence is the difference between a player closing your tab forever and them playing your game on a transatlantic flight with zero Wi-Fi.
What is IndexedDB State Management?
Definition: IndexedDB State Management is an asynchronous, transactional, object-oriented client-side database built into modern web browsers. It allows browser games to store structured clones of JavaScript objects, raw binary
ArrayBufferdata, and complex game assets locally, enabling seamless offline play and large-scale save-state persistence without hitting strict storage ceilings.
Unlike traditional relational databases, IndexedDB is schemaless (apart from object stores and indexes), runs entirely client-side, and handles enormous volumes of data. According to recent developer discussions on GitHub and r/webdev, community consensus points to IndexedDB as the definitive standard for modern web game state management, completely superseding WebSQL and legacy cookie storage.
Why LocalStorage Fails for Browser Games
| Feature | localStorage | IndexedDB |
|---|---|---|
| API Type | Synchronous (blocks main thread) | Asynchronous (Promise-based) |
| Capacity Limit | Typically 5 MB | Up to 60%+ of available disk space |
| Data Types | Strings only (JSON.stringify heavy) | Structured clones, Blobs, Binary data |
| Indexing | None (full scans required) | High-speed keypaths and indexes |
| Transactions | None | Fully ACID-compliant atomic transactions |
Architectural Blueprint: The Binary Save State
When building high-performance indie browser games, serialising massive JSON objects every time a player triggers an auto-save can cause nasty frame drops. Garbage collection spikes and string conversion overhead will murder your buttery-smooth 60 FPS frame rate.
The modern indie developer solution? Binary save states using ArrayBuffer and TypedArrays.
// Example: Storing a compressed binary game state directly into IndexedDB
async function saveGameBinary(slotId, binaryData) {
const db = await openGameDatabase();
const tx = db.transaction('save_states', 'readwrite');
const store = tx.objectStore('save_states');
// binaryData is an ArrayBuffer or Uint8Array representing raw game RAM
const record = {
id: slotId,
timestamp: Date.now(),
payload: binaryData
};
await store.put(record);
return tx.complete;
}
By packing player coordinates, inventory grids, and world seeds into raw binary arrays, your save files shrink drastically, and writing them to IndexedDB happens asynchronously off the main thread.
Conquering Quota Management and Eviction Wars
One of the biggest anxieties among PWA developers is browser storage eviction. If a user’s phone runs low on disk space, the browser can ruthlessly wipe out your IndexedDB storage without asking for permission.
Recent deep-dives from Chrome developer relations and community engineering logs highlight several vital tactics to bulletproof your game against sudden data purges:
1. Request Persistent Storage Early
Modern browsers support the Storage API, which lets you ask the user's browser to grant your origin permanent exemption from automatic heuristic eviction.
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const isPersistent = await navigator.storage.persisted();
if (!isPersistent) {
const granted = await navigator.storage.persist();
console.log(`Persistent storage granted: ${granted}`);
}
}
}
2. Monitor Storage Quota Usage
Never assume you have infinite room. Use navigator.storage.estimate() to check your current footprint against the device's maximum quota before writing heavy asset caches or multiple save slots.
async function checkStorageHealth() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
const percentage = ((usage / quota) * 100).toFixed(2);
console.log(`Storage used: ${usage} bytes out of ${quota} bytes (${percentage}%)`);
if (percentage > 80) {
triggerSaveCompressionOrWarning();
}
}
}
Practical Strategy Guide for Indie Devs
1. Wrap Your Database Calls: Raw IndexedDB boilerplate is notoriously verbose (cursor iterations, version change events, upgrade callbacks). Utilise lightweight, Promise-based wrappers like idb by Jake Archibald to keep your codebase clean and maintainable.
2. Isolate Asset Caching: Do not mix your binary game save states with static asset caches (sprites, audio loops). Let the Service Worker handle static asset caching via the Cache API, while IndexedDB strictly handles player state, progression trees, and dynamic configuration data.
3. Handle Version Upgrades Gracefully: When you push a game update that alters the save schema, ensure your onupgradeneeded event handler migrates old JSON structures into the new binary format smoothly, preventing corrupted save states for returning players.