The Web Storage API: Local vs Session Storage
A complete comparison of the Web Storage API covering localStorage vs sessionStorage. Covers persistence differences, storage events, quota limits, when to use each, migration patterns, combined storage strategies, typed wrappers, and building a unified storage manager.
The Web Storage API provides two storage mechanisms: localStorage for persistent data across browser sessions and sessionStorage for data tied to a single tab or window. Both share the same interface but differ in lifetime, scope, and cross-tab behavior.
For deep dives on each, see JS localStorage API Guide and JS sessionStorage API Guide.
Core Differences
| Feature | localStorage | sessionStorage |
|---|---|---|
| Persistence | Until explicitly deleted | Until tab/window closes |
| Scope | Shared across all tabs on same origin | Isolated to the specific tab |
| Storage event | Fires in other tabs | Does not fire in other tabs |
| Quota | Typically 5-10 MB per origin | Typically 5-10 MB per origin |
| Survives page refresh | Yes | Yes |
| Survives browser restart | Yes | No |
| Duplicating a tab | Not affected | New tab gets a copy of the data |
Shared API Surface
Both localStorage and sessionStorage implement the Storage interface:
// These methods work identically on both
function demonstrateStorageAPI(storage) {
// Set items
storage.setItem("username", "alice");
storage.setItem("theme", "dark");
// Get items
const username = storage.getItem("username"); // "alice"
const missing = storage.getItem("nonexistent"); // null
// Check length
console.log(storage.length); // 2
// Get key by index
console.log(storage.key(0)); // "username" (order not guaranteed)
// Remove specific item
storage.removeItem("theme");
// Clear everything
storage.clear();
}
// Same API, different behavior
demonstrateStorageAPI(localStorage);
demonstrateStorageAPI(sessionStorage);When to Use Each
// localStorage: data that persists across sessions
localStorage.setItem("preferred-language", "en");
localStorage.setItem("cookie-consent", JSON.stringify({
accepted: true,
timestamp: Date.now(),
}));
// sessionStorage: data for the current workflow only
sessionStorage.setItem("checkout-step", "2");
sessionStorage.setItem("form-draft", JSON.stringify({
name: "Alice",
email: "alice@example.com",
}));| Use Case | Best Storage | Reason |
|---|---|---|
| User preferences (theme, language) | localStorage | Must survive browser restart |
| Authentication tokens | Neither (use httpOnly cookies) | Storage is accessible to XSS |
| Form drafts during checkout | sessionStorage | Discard after tab closes |
| Shopping cart | localStorage | Persist for returning users |
| Wizard/multi-step form progress | sessionStorage | Scoped to the current flow |
| Feature flags cache | localStorage | Avoid re-fetching on every visit |
| Scroll position for back navigation | sessionStorage | Only relevant for current session |
Storage Events for Cross-Tab Sync
localStorage fires storage events in other tabs on the same origin. sessionStorage does not fire cross-tab events.
// Tab A: write data
localStorage.setItem("sync-message", JSON.stringify({
type: "theme-changed",
value: "dark",
timestamp: Date.now(),
}));
// Tab B: listen for changes
window.addEventListener("storage", (event) => {
if (event.storageArea !== localStorage) return;
if (event.key !== "sync-message") return;
const message = JSON.parse(event.newValue);
console.log("Received from another tab:", message);
if (message.type === "theme-changed") {
document.documentElement.setAttribute("data-theme", message.value);
}
});Cross-Tab Communication via localStorage
class CrossTabChannel {
constructor(channelName) {
this.channelName = `channel:${channelName}`;
this.listeners = new Set();
window.addEventListener("storage", (event) => {
if (event.key !== this.channelName) return;
if (!event.newValue) return;
try {
const message = JSON.parse(event.newValue);
for (const listener of this.listeners) {
listener(message);
}
} catch {
// Ignore malformed messages
}
});
}
send(data) {
const message = {
data,
id: crypto.randomUUID(),
timestamp: Date.now(),
};
localStorage.setItem(this.channelName, JSON.stringify(message));
// Remove immediately so the same message can be sent again
localStorage.removeItem(this.channelName);
}
onMessage(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
}
// Tab A
const channel = new CrossTabChannel("app-events");
channel.send({ type: "user-logout" });
// Tab B
const channel2 = new CrossTabChannel("app-events");
channel2.onMessage((msg) => {
if (msg.data.type === "user-logout") {
window.location.href = "/auth";
}
});Typed Storage Wrapper
Both storage APIs only store strings. A typed wrapper handles serialization:
class TypedStorage {
constructor(storage) {
this.storage = storage;
}
get(key, fallback = null) {
const raw = this.storage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return raw; // Return raw string if not valid JSON
}
}
set(key, value) {
if (value === undefined) {
this.storage.removeItem(key);
return;
}
this.storage.setItem(key, JSON.stringify(value));
}
remove(key) {
this.storage.removeItem(key);
}
has(key) {
return this.storage.getItem(key) !== null;
}
keys() {
const result = [];
for (let i = 0; i < this.storage.length; i++) {
result.push(this.storage.key(i));
}
return result;
}
clear() {
this.storage.clear();
}
getUsedBytes() {
let total = 0;
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i);
total += key.length + this.storage.getItem(key).length;
}
return total * 2; // UTF-16
}
}
// Create typed instances for each storage
const local = new TypedStorage(localStorage);
const session = new TypedStorage(sessionStorage);
// Store and retrieve typed data
local.set("settings", { theme: "dark", fontSize: 16 });
const settings = local.get("settings"); // { theme: "dark", fontSize: 16 }
session.set("cart", [{ id: 1, qty: 2 }]);
const cart = session.get("cart"); // [{ id: 1, qty: 2 }]Migration Between Storage Types
function migrateToLocal(keys) {
const migrated = [];
keys.forEach((key) => {
const value = sessionStorage.getItem(key);
if (value !== null) {
localStorage.setItem(key, value);
sessionStorage.removeItem(key);
migrated.push(key);
}
});
return migrated;
}
function migrateToSession(keys) {
const migrated = [];
keys.forEach((key) => {
const value = localStorage.getItem(key);
if (value !== null) {
sessionStorage.setItem(key, value);
localStorage.removeItem(key);
migrated.push(key);
}
});
return migrated;
}
// Migrate cart to localStorage when user creates an account
function onUserSignup() {
migrateToLocal(["cart", "wishlist"]);
}
// Move sensitive data to sessionStorage after page load
function onPageLoad() {
migrateToSession(["temp-token", "csrf-nonce"]);
}Unified Storage Manager
class StorageManager {
constructor(namespace = "app") {
this.namespace = namespace;
this.local = new TypedStorage(localStorage);
this.session = new TypedStorage(sessionStorage);
this.schema = new Map();
}
define(key, options = {}) {
this.schema.set(key, {
storage: options.persistent ? "local" : "session",
default: options.default !== undefined ? options.default : null,
ttl: options.ttl || 0, // milliseconds, 0 = no expiry
validate: options.validate || null,
});
return this;
}
getStore(key) {
const config = this.schema.get(key);
if (!config) throw new Error(`Unknown storage key: ${key}`);
return config.storage === "local" ? this.local : this.session;
}
prefixedKey(key) {
return `${this.namespace}:${key}`;
}
get(key) {
const config = this.schema.get(key);
if (!config) throw new Error(`Unknown storage key: ${key}`);
const store = this.getStore(key);
const fullKey = this.prefixedKey(key);
const entry = store.get(fullKey);
if (entry === null) return config.default;
// Check TTL
if (config.ttl > 0 && entry.storedAt) {
if (Date.now() - entry.storedAt > config.ttl) {
store.remove(fullKey);
return config.default;
}
}
return entry.value !== undefined ? entry.value : entry;
}
set(key, value) {
const config = this.schema.get(key);
if (!config) throw new Error(`Unknown storage key: ${key}`);
if (config.validate && !config.validate(value)) {
throw new Error(`Validation failed for key: ${key}`);
}
const store = this.getStore(key);
const fullKey = this.prefixedKey(key);
store.set(fullKey, {
value,
storedAt: Date.now(),
});
}
remove(key) {
const store = this.getStore(key);
store.remove(this.prefixedKey(key));
}
clearNamespace() {
for (const key of this.schema.keys()) {
this.remove(key);
}
}
getStats() {
return {
localUsed: this.local.getUsedBytes(),
sessionUsed: this.session.getUsedBytes(),
definedKeys: this.schema.size,
};
}
}
// Usage
const storage = new StorageManager("myapp");
storage
.define("theme", { persistent: true, default: "system" })
.define("language", { persistent: true, default: "en" })
.define("checkout-step", { persistent: false, default: 1 })
.define("api-cache", {
persistent: true,
ttl: 5 * 60 * 1000, // 5 minutes
default: null,
})
.define("font-size", {
persistent: true,
default: 16,
validate: (v) => typeof v === "number" && v >= 10 && v <= 32,
});
storage.set("theme", "dark");
storage.set("checkout-step", 3);
console.log(storage.get("theme")); // "dark" (from localStorage)
console.log(storage.get("checkout-step")); // 3 (from sessionStorage)Quota and Error Handling
function safeSetItem(storage, key, value) {
try {
storage.setItem(key, value);
return true;
} catch (error) {
if (
error instanceof DOMException &&
(error.name === "QuotaExceededError" ||
error.code === 22 ||
error.code === 1014) // Firefox
) {
console.warn("Storage quota exceeded for key:", key);
return false;
}
throw error;
}
}
function getStorageQuotaEstimate(storage) {
let used = 0;
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
used += (key.length + storage.getItem(key).length) * 2;
}
return {
usedBytes: used,
usedKB: (used / 1024).toFixed(2),
usedMB: (used / (1024 * 1024)).toFixed(4),
};
}
console.log("localStorage:", getStorageQuotaEstimate(localStorage));
console.log("sessionStorage:", getStorageQuotaEstimate(sessionStorage));Rune AI
Key Insights
- Persistence is the key difference: localStorage survives browser restarts while sessionStorage is cleared when the tab closes, making each suited to different use cases
- Storage events enable cross-tab sync: Only localStorage fires
storageevents in other tabs, enabling real-time theme sync, logout propagation, and messaging - Typed wrappers eliminate serialization bugs: Always use
JSON.parse/JSON.stringifywrappers since both storage types only store strings natively - Quota errors must be caught: Wrap all
setItemcalls in try/catch to handleQuotaExceededErrorgracefully with cleanup or fallback strategies - Security requires caution: Never store authentication tokens or sensitive data in Web Storage since it is fully accessible to any script running on the page
Frequently Asked Questions
Can sessionStorage data leak to other tabs?
Is localStorage data accessible to XSS attacks?
What happens when storage quota is exceeded?
Do storage events fire in the same tab that made the change?
Should I use Web Storage or IndexedDB?
Conclusion
The Web Storage API provides two mechanisms suited to different lifetimes: localStorage for persistent cross-session data and sessionStorage for tab-scoped temporary data. Use typed wrappers for consistent serialization, storage events for cross-tab communication, and always handle quota errors. For detailed coverage, see JS localStorage API Guide and JS sessionStorage API Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.