Modern browsers offer multiple storage mechanisms, each with different tradeoffs. Choosing the right one depends on data size, structure, persistence requirements, and whether you need server-sync. Here's a practical comparison for Firefox web developers.
The Storage Options
| localStorage | sessionStorage | Cookies | IndexedDB | Cache API | OPFS | |
|---|---|---|---|---|---|---|
| Capacity | ~5MB | ~5MB | ~4KB | Hundreds of MB | Hundreds of MB | Gigabytes |
| Data type | Strings only | Strings only | Strings | Structured | Request/Response | Files/Blobs |
| Async | No | No | No | Yes | Yes | Yes |
| Persists | Until cleared | Tab session | Configurable | Until cleared | Until cleared | Until cleared |
| Server access | No | No | Yes (sent in headers) | No | No | No |
| Available in workers | No | No | No | Yes | Yes | Yes |
localStorage
Use for: small amounts of simple persistent data
// Store
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
// Read
const theme = localStorage.getItem('theme'); // 'dark' or null
const user = JSON.parse(localStorage.getItem('user'));
// Delete
localStorage.removeItem('theme');
localStorage.clear(); // Delete everything
// Iterate
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(key, localStorage.getItem(key));
}
Pitfalls:
- Synchronous — blocks the main thread (imperceptible for small data, noticeable for large data)
- Strings only — must JSON.stringify/parse objects
- 5MB limit — JSON stringify is inefficient; hits limit quickly with complex objects
- Shared across all tabs (same origin) — use
storageevent to sync tabs
// Listen for changes from other tabs
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
applyTheme(event.newValue);
}
});
sessionStorage
Same API as localStorage, but:
- Cleared when the tab/window closes
- Each tab has its own sessionStorage (not shared)
- Persists through page reloads within the same tab
// Good for: multi-step forms
sessionStorage.setItem('form-step-1', JSON.stringify(formData));
// Good for: per-session tokens (not persistent auth)
sessionStorage.setItem('session-token', token);
Cookies
Sent to the server with every request. Use for authentication and server-side session state.
// JS can read/write non-HttpOnly cookies
document.cookie = 'name=value; path=/; max-age=3600; secure; samesite=strict';
// Reading (returns all cookies as one string)
const cookies = document.cookie; // "name=value; other=thing"
const value = cookies
.split('; ')
.find(row => row.startsWith('name='))
?.split('=')[1];
For security: prefer HttpOnly (only server can read), Secure (HTTPS only), and SameSite=Strict (no cross-site sending).
Don't use cookies for client-side-only data — they're included in every HTTP request (bandwidth overhead).
IndexedDB
Use for: large structured data, offline apps, complex queries
See the full IndexedDB guide for details. Quick summary:
// idb library (recommended wrapper)
import { openDB } from 'idb';
const db = await openDB('MyApp', 1, {
upgrade(db) {
db.createObjectStore('tasks', { keyPath: 'id' });
},
});
await db.add('tasks', { id: 1, title: 'Buy groceries', done: false });
const tasks = await db.getAll('tasks');
Cache API (for Service Workers)
Use for: caching network requests, offline support
// In service worker
const cache = await caches.open('app-v1');
await cache.addAll(['/index.html', '/app.js', '/app.css']);
// Serve from cache
const cached = await caches.match(request);
if (cached) return cached;
return fetch(request);
// Cache a response
const response = await fetch(request);
cache.put(request, response.clone());
return response;
The Cache API stores full HTTP request/response pairs — headers, body, status. It's designed for service worker caching strategies, not arbitrary data storage.
Origin Private File System (OPFS)
Use for: large files, binary data, file-like storage
Firefox 111+ supports OPFS — a private virtual file system per origin:
// Get root directory
const root = await navigator.storage.getDirectory();
// Create a file
const fileHandle = await root.getFileHandle('data.bin', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(binaryData);
await writable.close();
// Read a file
const file = await fileHandle.getFile();
const content = await file.arrayBuffer();
// Available in workers too (synchronous in workers!)
OPFS is great for: SQLite databases in the browser (via WebAssembly), large binary files, CAD tools, photo editors.
Storage Quota
Firefox manages storage per origin. Check available quota:
const estimate = await navigator.storage.estimate();
console.log('Used:', estimate.usage, 'bytes');
console.log('Quota:', estimate.quota, 'bytes');
console.log('Available:', estimate.quota - estimate.usage, 'bytes');
Request persistent storage (won't be evicted by the browser under storage pressure):
const granted = await navigator.storage.persist();
console.log('Persistent storage:', granted); // true/false
Firefox DevTools: Storage Inspector
View and manage all storage:
- DevTools → Storage tab
- Left panel: Cookies, localStorage, sessionStorage, IndexedDB, Cache Storage
- Click any entry to view/edit/delete
For IndexedDB: navigate database → object store → records. You can add, edit, and delete records directly.
Decision Guide
Small data (< 100KB) + need sync between tabs → localStorage
Per-tab session data → sessionStorage
Must reach server with requests → Cookies
Large structured data + queries → IndexedDB
Cache network responses → Cache API
Large files + binary data → OPFS
For most web apps: localStorage for preferences, IndexedDB for app data, Cookies for auth. Service workers + Cache API for offline support.
Top comments (0)