Introduction
Client-side storage is a fundamental requirement for modern web applications — offline caches, user preferences, draft data, and session state all need to persist in the browser. For years, localStorage has been the simplest go-to solution, but its limitations become painful as applications grow. This article compares localStorage, raw IndexedDB, and the localForage wrapper library, providing practical guidance for choosing the right storage engine.
The Limits of localStorage
localStorage offers a straightforward synchronous key-value API — setItem(key, value) and getItem(key) — but it comes with hard constraints:
| Constraint | Detail |
|---|---|
| Maximum size | ~5 MB per origin (varies slightly across browsers) |
| Synchronous API | Blocks the main thread during read/write operations |
| String-only values | Requires manual JSON serialization for objects |
| No indexing | Cannot query or filter stored data efficiently |
In a single-page application that maintains a large offline cache or complex application state, hitting the 5 MB ceiling is common. Moreover, synchronous writes that block rendering degrade user experience, especially on slow devices.
IndexedDB: Powerful but Complex
IndexedDB is the browser’s low-level NoSQL database. It offers:
- Virtually unlimited storage (browser-dependent, typically hundreds of MB to GB)
- Fully asynchronous API that does not block the UI thread
- Structured data support — objects, blobs, and arrays without serialization
- Indexed queries for efficient data retrieval
However, raw IndexedDB is notoriously verbose. A simple read operation requires opening a connection, creating a transaction, obtaining an object store, and handling success/error events:
const request = indexedDB.open('MyDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('store', 'readonly');
const store = tx.objectStore('store');
const get = store.get('myKey');
get.onsuccess = () => console.log(get.result);
};
This complexity makes IndexedDB error-prone and unpleasant for simple key-value use cases.
localForage: The Best of Both Worlds
localForage is a lightweight JavaScript library that wraps IndexedDB (with WebSQL and localStorage as fallbacks) behind a simple, asynchronous API inspired by localStorage. It automatically selects the best available storage engine.
npm install localforage
Basic Usage
import localforage from 'localforage';
// Store a value (supports objects, blobs, arrays)
await localforage.setItem('user', { name: 'Alice', role: 'admin' });
// Retrieve the value
const user = await localforage.getItem('user');
console.log(user.name); // "Alice"
// Remove a key
await localforage.removeItem('user');
// Clear all data
await localforage.clear();
// Get storage length
const len = await localforage.length();
The API is promise-based, so it works seamlessly with async/await or .then() chains.
Configuration
You can configure the driver priority, database name, and store name:
localforage.config({
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
name: 'MyApp',
storeName: 'app_cache',
version: 1.0,
description: 'Offline cache for MyApp'
});
Fallback Behavior
localForage detects the best available driver in this order:
- IndexedDB — preferred for its capacity and async model
- WebSQL — deprecated but still available in some browsers
- localStorage — last resort, with its 5 MB limitation
This ensures your application works everywhere while maximizing storage capacity when modern APIs are present.
Code Example: Caching API Responses
import localforage from 'localforage';
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function fetchWithCache(url) {
const cached = await localforage.getItem(url);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
await localforage.setItem(url, { data, timestamp: Date.now() });
return data;
}
This pattern reduces network requests while keeping the UI responsive — the async I/O never blocks rendering.
When to Use What
| Scenario | Recommended Storage |
|---|---|
| Simple key-value, small data (< 5 MB) | localStorage or localForage |
| Large JSON blobs, offline caches | localForage (IndexedDB backend) |
| Complex queries, indexes, large datasets | Raw IndexedDB |
| Cross-tab synchronization | IndexedDB + BroadcastChannel |
| Temporary session-only state | sessionStorage |
Conclusion
localStorage remains useful for truly small amounts of data, but its synchronous nature and 5 MB cap make it unsuitable for modern, data-intensive applications. Raw IndexedDB is powerful but overly complex for typical key-value scenarios. localForage bridges the gap by providing a clean, promise-based API that defaults to IndexedDB while gracefully degrading. For most client-side storage needs, localForage is the pragmatic choice.
