Browser Storage
Browsers provide several ways to store data on the client. Each mechanism has different lifetimes, size limits, and use cases. Choosing the right one depends on what you are storing, how long it needs to persist, and whether the server needs access.
localStorage
localStorage provides persistent key-value storage that survives browser restarts. Data stays until the user clears it or your code removes it.
// Store a value
localStorage.setItem('theme', 'dark');
// Retrieve a value
const theme = localStorage.getItem('theme'); // 'dark'
// Remove a single item
localStorage.removeItem('theme');
// Clear everything for this origin
localStorage.clear();
Characteristics
- Persistent: data survives page reloads and browser restarts
- Synchronous: blocks the main thread on read/write
- ~5 MB limit: per origin (protocol + domain + port)
- Strings only: you must serialize objects yourself
// Storing objects requires JSON serialization
const user = { name: 'Ada', role: 'engineer' };
localStorage.setItem('user', JSON.stringify(user));
const stored = JSON.parse(localStorage.getItem('user'));
console.log(stored.name); // 'Ada'
The storage Event
When localStorage changes in one tab, other tabs on the same origin receive a storage event. This enables cross-tab communication.
window.addEventListener('storage', (event) => {
console.log(`Key: ${event.key}`);
console.log(`Old: ${event.oldValue}`);
console.log(`New: ${event.newValue}`);
});
sessionStorage
sessionStorage works like localStorage but scopes data to a single browser tab and session.
sessionStorage.setItem('formDraft', 'half-written email...');
// Available in this tab only
const draft = sessionStorage.getItem('formDraft');
Characteristics
- Per-tab: each tab gets its own storage, even for the same URL
- Cleared on close: data disappears when the tab closes
- Same API: setItem, getItem, removeItem, clear
- ~5 MB limit: same as localStorage
When to Use sessionStorage
- Temporary form data that should not persist if the user closes the tab
- Wizard/multi-step flow state
- Data you want isolated between tabs (e.g., different items being edited)
IndexedDB
IndexedDB is a full database in the browser. It stores structured data, supports indexes, and handles large amounts of data asynchronously.
// Open (or create) a database
const request = indexedDB.open('myApp', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
store.createIndex('title', 'title', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// Write data
const tx = db.transaction('notes', 'readwrite');
tx.objectStore('notes').add({ title: 'First note', body: 'Hello IndexedDB' });
// Read data
const readTx = db.transaction('notes', 'readonly');
const getReq = readTx.objectStore('notes').get(1);
getReq.onsuccess = () => {
console.log(getReq.result);
};
};
Characteristics
- Asynchronous: does not block the main thread
- Large storage: hundreds of MB or more (browser-dependent)
- Structured data: stores JavaScript objects directly, no serialization needed
- Indexes: query by any indexed field, not just key
- Transactions: groups of operations that succeed or fail together
Simplifying IndexedDB
The raw IndexedDB API is verbose. Libraries like idb provide a Promise-based wrapper.
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
},
});
await db.add('notes', { title: 'Clean note', body: 'Much simpler' });
const note = await db.get('notes', 1);
Cookies
Cookies are the oldest storage mechanism. They are sent to the server with every HTTP request, which makes them fundamentally different from the Storage APIs.
// Set a cookie
document.cookie = 'session=abc123; path=/; max-age=3600; Secure; SameSite=Strict';
// Read cookies (returns all as one string)
console.log(document.cookie); // 'session=abc123; theme=dark'
Cookies vs Storage
| Feature | Cookies | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Sent to server | Yes | No | No | No |
| Size limit | ~4 KB | ~5 MB | ~5 MB | Large |
| Lifetime | Configurable | Permanent | Tab close | Permanent |
| API | String-based | Key-value | Key-value | Database |
| Synchronous | Yes | Yes | Yes | No |
When to Use Cookies
- Authentication tokens (HttpOnly cookies the server reads)
- Server-side preferences (language, region)
- Tracking (third-party cookies, though these are being phased out)
The Storage API
The Storage API lets you estimate how much space your origin is using and how much is available.
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
console.log(`Used: ${(estimate.usage / 1024 / 1024).toFixed(2)} MB`);
console.log(`Quota: ${(estimate.quota / 1024 / 1024).toFixed(2)} MB`);
}
Persistent Storage
By default, the browser can evict stored data under storage pressure. You can request persistent storage.
const persistent = await navigator.storage.persist();
if (persistent) {
console.log('Storage will not be evicted');
}
Clearing Data
Users can clear storage through browser settings, but your app should also manage its own data.
// Clear all localStorage
localStorage.clear();
// Clear all sessionStorage
sessionStorage.clear();
// Delete an IndexedDB database
indexedDB.deleteDatabase('myApp');
// Delete a cookie by setting it expired
document.cookie = 'session=; max-age=0';
Storage Eviction
Browsers may clear storage when disk space is low. The eviction order is typically: least recently used origins first, non-persistent storage before persistent.
When to Use Each
- localStorage: user preferences (theme, language), small cached data, anything that should persist across sessions
- sessionStorage: temporary per-tab state, form drafts, wizard progress
- IndexedDB: large datasets, offline data, structured data you need to query
- Cookies: authentication, server-readable preferences, anything the server needs on every request
Common Pitfalls
- Storing sensitive data in localStorage: it is accessible to any JavaScript on the page, including XSS attacks. Use HttpOnly cookies for auth tokens.
- Forgetting JSON.parse/stringify: localStorage stores strings only. Storing an object without serializing it gives you
[object Object]. - Exceeding the 5 MB limit: localStorage throws a
QuotaExceededError. Always wrap setItem in a try-catch. - Blocking the main thread: localStorage is synchronous. Storing or reading large amounts of data can freeze the UI. Use IndexedDB for large data.
- Assuming data is always there: users clear storage, browsers evict it. Always handle the case where stored data is missing.
- Using cookies for large data: cookies are sent with every HTTP request. Storing large values in cookies wastes bandwidth on every request.
Key Takeaways
- localStorage persists across sessions, sessionStorage is per-tab and cleared on close
- IndexedDB is the right choice for large or structured data because it is async and supports indexes
- Cookies are the only storage mechanism sent to the server automatically
- Always serialize objects before storing in localStorage and deserialize when reading
- Use the Storage API to check quota and request persistent storage
- Never store sensitive data (tokens, passwords) in localStorage or sessionStorage