IndexedDB & Structured Storage
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐๏ธ IndexedDB = a filing cabinet in your browser with custom tab systems.
The filing cabinet analogy
Object stores are drawers โ each drawer holds a specific type of document (users, orders, messages). You decide the drawer names in onupgradeneeded.
Keys are folder names. You find a folder by its exact name (key). Fast O(log n) lookup.
Indexesare custom tab systems. Without an index on "email", finding a user by email means flipping through every folder (scan). With an index, there's a separate alphabetical tab system that points directly to the right folder.
Transactions are a locked-room session. You check out folders, make changes, and check them back in atomically โ no one else can interfere mid-operation.
Why async when localStorage is sync?
localStorage blocks the main thread โ a 10MB read freezes your UI. IndexedDB runs on a background thread โ reads happen without blocking rendering. This is why IDB uses callbacks/IDBRequest (and modern libraries wrap it in Promises). The async overhead is worth it for large datasets.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Open a database and define object stores in onupgradeneeded
Challenge
Step through all 5 patterns to their final step. Understand schema, CRUD, indexes, transactions, and when to use IDB.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: Why is IndexedDB asynchronous while localStorage is synchronous?
localStorage is a simple in-memory key-value store backed by disk. Reads are fast and block briefly โ acceptable for small data. IndexedDB is a real database engine (built on LevelDB/SQLite internally). Queries, index lookups, and large data reads take meaningful time. Running them synchronously on the main thread would cause visible jank. IDB's async model keeps the UI responsive during database operations.
Q: When should you use IndexedDB vs Cache API?
Cache API: for caching HTTP responses (HTML, CSS, JS, images) โ designed to work with Service Workers for offline support. Key = URL, value = Response object. IndexedDB: for structured application data (user profiles, messages, drafts, shopping cart) โ needs to be queried, indexed, and transactionally updated. Different tools for different purposes.
Q: How do you handle IndexedDB schema migrations?
Increment the version number in indexedDB.open(name, version). The onupgradeneeded handler receives e.oldVersion and e.newVersion. Use a switch/if chain to apply incremental migrations: if (oldVersion < 2) addIndex(), if (oldVersion < 3) addStore(). Never skip versions โ a user upgrading from v1 to v3 must go through v2 migrations.
Use idb in production
| 1 | import { openDB } from 'idb'; |
| 2 | ย |
| 3 | const db = await openDB('myApp', 2, { |
| 4 | upgrade(db, oldVersion) { |
| 5 | if (oldVersion < 1) { |
| 6 | const store = db.createObjectStore('users', { keyPath: 'id' }); |
| 7 | store.createIndex('by-email', 'email', { unique: true }); |
| 8 | } |
| 9 | if (oldVersion < 2) { |
| 10 | db.createObjectStore('orders', { keyPath: 'id' }); |
| 11 | } |
| 12 | }, |
| 13 | }); |
| 14 | ย |
| 15 | // Clean async/await API: |
| 16 | await db.put('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' }); |
| 17 | const user = await db.get('users', 'u1'); |
| 18 | const all = await db.getAll('users'); |
| 19 | await db.delete('users', 'u1'); |
The Deep Dive
โ Spec refs, engine internals, the minutiae.Dexie.js โ rich query API
The idb library is a thin Promise wrapper. Dexie.js provides a full ORM-like API with rich querying:
| 1 | import Dexie from 'dexie'; |
| 2 | ย |
| 3 | const db = new Dexie('myApp'); |
| 4 | db.version(1).stores({ |
| 5 | users: '++id, email, role', // ++ = auto-increment |
| 6 | orders: '++id, userId, status', |
| 7 | }); |
| 8 | ย |
| 9 | // Rich queries |
| 10 | const admins = await db.users |
| 11 | .where('role').equals('admin') |
| 12 | .toArray(); |
| 13 | ย |
| 14 | const userOrders = await db.orders |
| 15 | .where('userId').equals(userId) |
| 16 | .and(o => o.status === 'pending') |
| 17 | .toArray(); |
Storage quota and persistence
Browsers allocate storage quotas dynamically. Typical limits:
- Chrome: up to 60% of available disk space, 20% per origin
- Firefox: up to 10% of free disk space
- Safari: 1GB by default (can request more)
Check and request persistence:
| 1 | // Check current usage |
| 2 | const estimate = await navigator.storage.estimate(); |
| 3 | console.log(estimate.quota); // bytes available |
| 4 | console.log(estimate.usage); // bytes used |
| 5 | ย |
| 6 | // Request persistent storage (user-prompted in some browsers) |
| 7 | const persisted = await navigator.storage.persist(); |
| 8 | // true = browser won't evict data automatically |
Origin Private File System (OPFS)
OPFS (available in modern browsers) provides a sandboxed, virtual filesystem. It's faster than IndexedDB for large binary data and enables running SQLite natively in the browser using the synchronous OPFS Worker API:
| 1 | // Access OPFS root directory |
| 2 | const root = await navigator.storage.getDirectory(); |
| 3 | ย |
| 4 | // Create a file |
| 5 | const fileHandle = await root.getFileHandle('data.bin', { create: true }); |
| 6 | const writable = await fileHandle.createWritable(); |
| 7 | await writable.write(buffer); |
| 8 | await writable.close(); |
| 9 | ย |
| 10 | // SQLite in a Worker via sqlite-wasm + OPFS |
| 11 | // (synchronous I/O available only in Workers) |
Interview Questions
โ Real questions from real interviews โ with answers.IDB is a real database engine that can block for meaningful time โ async keeps the main thread responsive.
Increment the version number โ onupgradeneeded fires with oldVersion and newVersion so you apply incremental migrations.
Transactions auto-commit when the microtask queue empties with no pending IDB requests โ an awaited fetch drains the queue.
IDB for structured app data you query/index; Cache API for HTTP response caching in service workers.
put() is an upsert (create or replace); add() fails with a ConstraintError if the key already exists.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.Why does IndexedDB use an asynchronous API?