Browser Storage & Caching
"Just use localStorage" is the hallmark of someone who hasn't dealt with storage at scale. Each browser storage mechanism has different capacity, persistence, access patterns, and security implications.
Storage Comparison
| Storage | Capacity | Persistence | Access | Sent to server |
|---|---|---|---|---|
| Cookies | ~4KB | Configurable expiry | Sync | Yes, every request |
| localStorage | ~5-10MB | Until cleared | Sync | No |
| sessionStorage | ~5-10MB | Until tab closes | Sync | No |
| IndexedDB | Hundreds of MB+ | Until cleared | Async | No |
| Cache API | Hundreds of MB+ | Until cleared | Async | No |
Cookies
Cookies are the only storage mechanism automatically sent with HTTP requests. This makes them the right choice for authentication tokens â and the wrong choice for almost everything else.
// Setting a secure cookie
document.cookie = 'session=abc123; Secure; HttpOnly; SameSite=Strict; Max-Age=86400; Path=/';Key attributes:
HttpOnlyâ inaccessible to JavaScript (XSS protection)Secureâ only sent over HTTPSSameSite=Strict|Lax|Noneâ CSRF protectionMax-Age/Expiresâ lifetime control
Auth tokens belong in HttpOnly cookies, not localStorage. A localStorage token is one XSS vulnerability away from being stolen.
localStorage & sessionStorage
Simple key-value string storage. Synchronous and blocking:
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
// Store objects by serializing
localStorage.setItem('prefs', JSON.stringify({ theme: 'dark', lang: 'en' }));
const prefs = JSON.parse(localStorage.getItem('prefs') ?? '{}');localStorage persists until explicitly cleared. sessionStorage is scoped to the browser tab and dies when it closes.
Pitfalls
- Synchronous â blocks the main thread. Storing/parsing large JSON payloads causes jank.
- String-only â everything is serialized. Type safety is your responsibility.
- Storage events â
window.addEventListener('storage', cb)fires in other tabs, not the current one. Useful for cross-tab sync. - No expiry â unlike cookies, there's no built-in TTL. You must implement expiration logic manually.
IndexedDB
A full asynchronous database in the browser. Use it for structured data that's too large for localStorage:
const request = indexedDB.open('myApp', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('articles', { keyPath: 'id' });
store.createIndex('date', 'publishedAt');
};
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('articles', 'readwrite');
tx.objectStore('articles').put({ id: '1', title: 'Hello', publishedAt: Date.now() });
};The raw API is verbose. In practice, use a wrapper like idb (by Jake Archibald) for a Promise-based interface:
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
db.createObjectStore('articles', { keyPath: 'id' });
},
});
await db.put('articles', { id: '1', title: 'Hello' });
const article = await db.get('articles', '1');Use IndexedDB for: offline data, large datasets, structured queries, blob storage.
Cache API
Designed for caching HTTP request/response pairs, primarily used with Service Workers for offline support:
const cache = await caches.open('v1');
// Cache a response
await cache.put('/api/data', new Response(JSON.stringify(data)));
// Cache from network
await cache.add('/api/data');
// Match a request
const response = await caches.match('/api/data');Caching Strategies
| Strategy | Description | Use case |
|---|---|---|
| Cache First | Check cache, fallback to network | Static assets, fonts |
| Network First | Try network, fallback to cache | API data that should be fresh |
| Stale While Revalidate | Serve cache, update in background | Content that can be briefly stale |
// Stale-while-revalidate in a Service Worker
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then(async (cache) => {
const cached = await cache.match(event.request);
const fetched = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetched;
})
);
});Decision Framework
Need to send data with every HTTP request? â Cookies
Need simple key-value persistence? â localStorage
Need tab-scoped temporary data? â sessionStorage
Need structured data or large storage? â IndexedDB
Need to cache network responses for offline? â Cache API
Need auth token storage? â HttpOnly Secure cookie (never localStorage)Interview Signal
Storage questions test security awareness and architectural judgment. Senior signals:
- Security-first thinking â knowing that auth tokens belong in HttpOnly cookies, understanding XSS implications of localStorage
- Performance awareness â knowing localStorage is synchronous and can block rendering
- Right tool for the job â not using localStorage for everything, understanding IndexedDB's strengths
- Offline strategy â articulating how Cache API + Service Workers enable offline experiences