localStorage and Browser Storage
1. localStorage and sessionStorage (setItem, getItem, removeItem)
| Method | Syntax | Description | Scope |
|---|---|---|---|
| setItem | storage.setItem(key, value) |
Stores key-value pair. Value converted to string. Throws QuotaExceededError if storage full. | Both |
| getItem | storage.getItem(key) |
Retrieves value by key. Returns null if key doesn't exist. |
Both |
| removeItem | storage.removeItem(key) |
Removes key-value pair. No error if key doesn't exist. | Both |
| clear | storage.clear() |
Removes all key-value pairs from storage. | Both |
| key | storage.key(index) |
Returns key name at index position. Returns null if out of range. |
Both |
| length | storage.length |
Number of stored key-value pairs. Read-only property. | Both |
| Storage Type | Lifetime | Capacity | Use Case |
|---|---|---|---|
| localStorage | Persists until explicitly deleted. Survives browser restart. | ~5-10MB per origin | User preferences, settings, cached data |
| sessionStorage | Cleared when page session ends (tab/window closed). Survives page reload. | ~5-10MB per origin | Temporary data, form state, single-session cache |
| Event Property | Type | Description |
|---|---|---|
| storage event | StorageEvent |
Fires on other tabs/windows when storage changes. Doesn't fire in tab that made change. |
| key | string |
Key that was changed. null if clear() called. |
| oldValue | string |
Previous value. null if new key. |
| newValue | string |
New value. null if item removed. |
| url | string |
URL of page that made the change. |
| storageArea | Storage |
Reference to localStorage or sessionStorage affected. |
Example: Basic storage operations
// Store data
localStorage.setItem("username", "john_doe");
localStorage.setItem("theme", "dark");
// Store objects (must stringify)
const user = { "id": 123, "name": "John" };
localStorage.setItem("user", JSON.stringify(user));
// Retrieve data
const username = localStorage.getItem("username");
console.log(username); // "john_doe"
// Retrieve and parse objects
const userStr = localStorage.getItem("user");
const userData = JSON.parse(userStr);
console.log(userData.name); // "John"
// Remove item
localStorage.removeItem("theme");
// Clear all
localStorage.clear();
// Check if key exists
if (localStorage.getItem("username") !== null) {
console.log("Username exists");
}
// Iterate all keys
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
Example: Helper functions and patterns
// Safe JSON storage wrapper
const storage = {
"set": (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error("Storage error:", error);
return false;
}
},
"get": (key, defaultValue = null) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
return defaultValue;
}
},
"remove": (key) => {
localStorage.removeItem(key);
}
};
// Usage
storage.set("settings", { "theme": "dark", "lang": "en" });
const settings = storage.get("settings", {});
// Storage with expiration
function setWithExpiry(key, value, ttl) {
const item = {
"value": value,
"expiry": Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
// Set with 1 hour expiry
setWithExpiry("token", "abc123", 3600000);
Example: Storage event for cross-tab communication
// Listen for storage changes from other tabs
window.addEventListener("storage", (event) => {
console.log("Storage changed in another tab");
console.log("Key:", event.key);
console.log("Old value:", event.oldValue);
console.log("New value:", event.newValue);
console.log("URL:", event.url);
console.log("Storage area:", event.storageArea === localStorage);
// React to specific changes
if (event.key === "theme") {
applyTheme(event.newValue);
}
// Handle logout in all tabs
if (event.key === "isLoggedIn" && event.newValue === "false") {
redirectToLogin();
}
});
// Trigger storage event in other tabs
localStorage.setItem("notification", JSON.stringify({
"message": "New update available",
"timestamp": Date.now()
}));
Note: Web Storage only stores strings - use
JSON.stringify() and JSON.parse() for objects. Storage is synchronous and can block UI - use IndexedDB for large data. Data is scoped to origin (protocol + domain + port). Storage event doesn't fire in the tab that made the change.
Warning: Storage can throw
QuotaExceededError when full - always wrap in try/catch. Private browsing modes may have reduced or disabled storage. Never store sensitive data (passwords, tokens) in localStorage - vulnerable to XSS. Use sessionStorage for temporary data that shouldn't persist.
2. IndexedDB (open, objectStore, transaction)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| open | indexedDB.open(name, version) |
Opens database. Returns IDBOpenDBRequest. Triggers upgradeneeded if version changes. | All Browsers |
| deleteDatabase | indexedDB.deleteDatabase(name) |
Deletes entire database. Returns IDBOpenDBRequest. | All Browsers |
| databases NEW | indexedDB.databases() |
Returns Promise with array of available databases and versions. | Modern Browsers |
| IDBDatabase Method | Description | Use Case |
|---|---|---|
| createObjectStore | Creates object store (table). Only in upgradeneeded event. Options: keyPath, autoIncrement. | Database schema creation |
| deleteObjectStore | Deletes object store. Only in upgradeneeded event. | Schema migration |
| transaction | Creates transaction. Parameters: storeNames (string/array), mode ("readonly"/"readwrite"). | Access data |
| close | Closes database connection. Good practice when done. | Cleanup |
| IDBObjectStore Method | Description | Returns |
|---|---|---|
| add | Adds new record. Fails if key exists. Returns IDBRequest. | IDBRequest |
| put | Adds or updates record. Overwrites if key exists. | IDBRequest |
| get | Gets record by key. Returns undefined if not found. |
IDBRequest |
| getAll | Gets all records. Optional query and count parameters. | IDBRequest |
| delete | Deletes record by key or key range. | IDBRequest |
| clear | Deletes all records in object store. | IDBRequest |
| count | Counts records. Optional key or key range parameter. | IDBRequest |
| openCursor | Opens cursor for iteration. Optional query and direction. | IDBRequest |
| createIndex | Creates index for queries. Only in upgradeneeded event. | IDBIndex |
| index | Gets existing index for querying. | IDBIndex |
Example: Opening database and creating schema
// Open database (creates if doesn't exist)
const request = indexedDB.open("MyDatabase", 1);
// Handle errors
request.onerror = (event) => {
console.error("Database error:", event.target.error);
};
// Create schema (only runs when version changes)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store with auto-incrementing key
const userStore = db.createObjectStore("users", {
"keyPath": "id",
"autoIncrement": true
});
// Create indexes for searching
userStore.createIndex("email", "email", { "unique": true });
userStore.createIndex("age", "age", { "unique": false });
userStore.createIndex("city", "city", { "unique": false });
// Create another object store
const productStore = db.createObjectStore("products", {
"keyPath": "sku"
});
productStore.createIndex("category", "category");
productStore.createIndex("price", "price");
};
// Success - database ready
request.onsuccess = (event) => {
const db = event.target.result;
console.log("Database opened successfully");
// Store db reference for later use
window.db = db;
};
Example: CRUD operations
// Add data
function addUser(userData) {
const transaction = db.transaction(["users"], "readwrite");
const store = transaction.objectStore("users");
const request = store.add(userData);
request.onsuccess = () => {
console.log("User added:", request.result); // Returns key
};
request.onerror = () => {
console.error("Add error:", request.error);
};
}
addUser({ "name": "John", "email": "john@example.com", "age": 30 });
// Get data by key
function getUser(id) {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const request = store.get(id);
request.onsuccess = () => {
if (request.result) {
console.log("User found:", request.result);
} else {
console.log("User not found");
}
};
}
getUser(1);
// Update data (put overwrites)
function updateUser(userData) {
const transaction = db.transaction(["users"], "readwrite");
const store = transaction.objectStore("users");
const request = store.put(userData); // Use put for update
request.onsuccess = () => {
console.log("User updated");
};
}
updateUser({ "id": 1, "name": "John Doe", "email": "john@example.com", "age": 31 });
// Delete data
function deleteUser(id) {
const transaction = db.transaction(["users"], "readwrite");
const store = transaction.objectStore("users");
const request = store.delete(id);
request.onsuccess = () => {
console.log("User deleted");
};
}
deleteUser(1);
// Get all data
function getAllUsers() {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const request = store.getAll();
request.onsuccess = () => {
console.log("All users:", request.result);
};
}
getAllUsers();
Example: Indexes and cursors
// Query by index
function getUserByEmail(email) {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const index = store.index("email");
const request = index.get(email);
request.onsuccess = () => {
console.log("User:", request.result);
};
}
getUserByEmail("john@example.com");
// Range queries with IDBKeyRange
function getUsersByAge(minAge, maxAge) {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const index = store.index("age");
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.getAll(range);
request.onsuccess = () => {
console.log("Users in age range:", request.result);
};
}
getUsersByAge(25, 35);
// Cursor for iteration
function iterateUsers() {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log("User:", cursor.value);
cursor.continue(); // Move to next record
} else {
console.log("Done iterating");
}
};
}
iterateUsers();
// Cursor with filtering
function filterUsers(callback) {
const transaction = db.transaction(["users"], "readonly");
const store = transaction.objectStore("users");
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (callback(cursor.value)) {
results.push(cursor.value);
}
cursor.continue();
} else {
console.log("Filtered results:", results);
}
};
}
filterUsers((user) => user.age > 25);
Example: Promise wrapper for easier async/await
// Helper to promisify IndexedDB
function promisifyRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Async wrapper class
class Database {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
}
async open(upgradeCallback) {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = upgradeCallback;
this.db = await promisifyRequest(request);
return this.db;
}
async add(storeName, data) {
const tx = this.db.transaction([storeName], "readwrite");
const store = tx.objectStore(storeName);
return promisifyRequest(store.add(data));
}
async get(storeName, key) {
const tx = this.db.transaction([storeName], "readonly");
const store = tx.objectStore(storeName);
return promisifyRequest(store.get(key));
}
async getAll(storeName) {
const tx = this.db.transaction([storeName], "readonly");
const store = tx.objectStore(storeName);
return promisifyRequest(store.getAll());
}
async delete(storeName, key) {
const tx = this.db.transaction([storeName], "readwrite");
const store = tx.objectStore(storeName);
return promisifyRequest(store.delete(key));
}
}
// Usage with async/await
async function useDatabase() {
const db = new Database("MyDB", 1);
await db.open((event) => {
const database = event.target.result;
database.createObjectStore("users", { "keyPath": "id", "autoIncrement": true });
});
const id = await db.add("users", { "name": "John", "email": "john@example.com" });
console.log("Added user:", id);
const user = await db.get("users", id);
console.log("User:", user);
const allUsers = await db.getAll("users");
console.log("All users:", allUsers);
}
Note: IndexedDB is asynchronous and event-based. Schema changes (createObjectStore, createIndex) only allowed in
upgradeneeded event. Use put() for upsert (insert or update), add() fails if key exists. IndexedDB can store large amounts of data (hundreds of MB to GB depending on browser).
Warning: All IndexedDB operations are transactional. Transaction auto-commits when all requests complete. Don't perform async operations (fetch, setTimeout) inside transaction handlers - transaction will close. Use indexes for queries - full table scans with cursors are slow on large datasets.
3. Cache API (caches.open, cache.put, cache.match)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| caches.open | caches.open(cacheName) |
Opens named cache. Creates if doesn't exist. Returns Promise<Cache>. | Modern Browsers |
| caches.match | caches.match(request, options) |
Searches all caches for matching request. Returns Promise<Response> or undefined. |
Modern Browsers |
| caches.has | caches.has(cacheName) |
Checks if named cache exists. Returns Promise<boolean>. | Modern Browsers |
| caches.delete | caches.delete(cacheName) |
Deletes named cache. Returns Promise<boolean> indicating if cache existed. | Modern Browsers |
| caches.keys | caches.keys() |
Returns Promise with array of cache names. | Modern Browsers |
| Cache Method | Description | Use Case |
|---|---|---|
| add | Fetches URL and stores response. Single request. | Cache single resource |
| addAll | Fetches array of URLs and stores responses. Atomic - fails if any fetch fails. | Cache multiple resources |
| put | Stores request-response pair directly. Full control over what's cached. | Cache custom responses |
| match | Searches cache for matching request. Returns Promise<Response> or undefined. |
Retrieve cached response |
| matchAll | Returns all matching responses. Optional request parameter for filtering. | Get multiple cached items |
| delete | Removes cached request-response pair. Returns Promise<boolean>. | Invalidate cache entry |
| keys | Returns all request keys in cache. Optional request parameter for filtering. | List cached URLs |
Example: Basic Cache API usage
// Open cache and add resources
async function cacheResources() {
const cache = await caches.open("my-cache-v1");
// Add single resource
await cache.add("/api/data");
// Add multiple resources (atomic operation)
await cache.addAll([
"/",
"/styles.css",
"/script.js",
"/logo.png"
]);
console.log("Resources cached");
}
// Check cache before fetching
async function fetchWithCache(url) {
// Try cache first
const cachedResponse = await caches.match(url);
if (cachedResponse) {
console.log("Cache hit:", url);
return cachedResponse;
}
// Cache miss - fetch from network
console.log("Cache miss:", url);
const response = await fetch(url);
// Store in cache for next time
const cache = await caches.open("my-cache-v1");
cache.put(url, response.clone());
return response;
}
// Usage
const response = await fetchWithCache("/api/users");
const data = await response.json();
Example: Cache strategies
// Cache First (good for static assets)
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open("static-v1");
cache.put(request, response.clone());
return response;
}
// Network First (good for dynamic data)
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open("dynamic-v1");
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
throw error;
}
}
// Stale While Revalidate (best of both)
async function staleWhileRevalidate(request) {
const cached = await caches.match(request);
const fetchPromise = fetch(request).then((response) => {
const cache = caches.open("swr-v1");
cache.then((c) => c.put(request, response.clone()));
return response;
});
return cached || fetchPromise;
}
// Cache with expiration
async function cacheWithExpiry(request, cacheName, maxAge) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
const cachedDate = new Date(cached.headers.get("date"));
const age = Date.now() - cachedDate.getTime();
if (age < maxAge) {
return cached; // Still fresh
}
}
// Expired or missing - fetch fresh
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
Example: Cache management
// List all caches
async function listCaches() {
const cacheNames = await caches.keys();
console.log("Caches:", cacheNames);
return cacheNames;
}
// Delete old cache versions
async function deleteOldCaches(currentVersion) {
const cacheNames = await caches.keys();
const deletePromises = cacheNames
.filter((name) => name !== currentVersion)
.map((name) => caches.delete(name));
await Promise.all(deletePromises);
console.log("Old caches deleted");
}
// Usage in Service Worker activate event
self.addEventListener("activate", (event) => {
event.waitUntil(
deleteOldCaches("my-cache-v2")
);
});
// List cached URLs
async function listCachedUrls(cacheName) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
const urls = requests.map((req) => req.url);
console.log("Cached URLs:", urls);
return urls;
}
// Remove specific cached item
async function removeCachedItem(url) {
const cache = await caches.open("my-cache-v1");
const deleted = await cache.delete(url);
console.log(`Cache entry deleted: ${deleted}`);
}
// Clear all caches
async function clearAllCaches() {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map((name) => caches.delete(name))
);
console.log("All caches cleared");
}
Note: Cache API stores Request-Response pairs, not just data. Primarily used with Service Workers for offline functionality. Responses must be
clone()d before caching because Response body can only be read once. Cache is persistent and separate from HTTP cache.
4. Cookies (document.cookie, expires, path)
| Property | Description | Format |
|---|---|---|
| document.cookie | Gets all cookies as semicolon-separated string. Setting adds/updates single cookie. | "name=value; name2=value2" |
| Cookie Attribute | Description | Example |
|---|---|---|
| expires | Expiration date (GMT format). Cookie deleted after this date. | expires=Wed, 01 Jan 2025 00:00:00 GMT |
| max-age | Lifetime in seconds. Overrides expires. Negative value deletes cookie. | max-age=3600 |
| path | URL path where cookie is accessible. Defaults to current path. | path=/ |
| domain | Domain where cookie is accessible. Includes subdomains if specified. | domain=.example.com |
| secure | Cookie only sent over HTTPS. Essential for sensitive data. | secure |
| httpOnly | Cookie inaccessible to JavaScript (document.cookie). Only server-side. Set by server. | httpOnly |
| samesite | CSRF protection. Values: Strict, Lax, None (requires Secure). |
samesite=Strict |
Example: Cookie helper functions
// Set cookie
function setCookie(name, value, days = 7, options = {}) {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
// Expiration
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
cookie += `; expires=${date.toUTCString()}`;
}
// Path (default to root)
cookie += `; path=${options.path || "/"}`;
// Domain
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
// Secure
if (options.secure) {
cookie += "; secure";
}
// SameSite
if (options.sameSite) {
cookie += `; samesite=${options.sameSite}`;
}
document.cookie = cookie;
}
// Get cookie
function getCookie(name) {
const cookies = document.cookie.split("; ");
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split("=");
if (decodeURIComponent(cookieName) === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}
// Delete cookie
function deleteCookie(name, options = {}) {
setCookie(name, "", -1, options);
}
// Get all cookies as object
function getAllCookies() {
const cookies = {};
document.cookie.split("; ").forEach((cookie) => {
const [name, value] = cookie.split("=");
cookies[decodeURIComponent(name)] = decodeURIComponent(value);
});
return cookies;
}
// Usage
setCookie("username", "john_doe", 30, {
"path": "/",
"secure": true,
"sameSite": "Strict"
});
const username = getCookie("username");
console.log(username); // "john_doe"
deleteCookie("username");
Note: Cookies have 4KB size limit per cookie. Always use
encodeURIComponent() for names and values. HttpOnly cookies can't be accessed via JavaScript - only set by server. Use SameSite=Strict or Lax for CSRF protection.
Warning: Cookies are sent with every HTTP request to the domain - impacts performance. Don't store sensitive data in client-accessible cookies.
Secure flag is mandatory for SameSite=None. Deleting cookie requires matching path and domain of original cookie.
5. Origin Private File System API
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| getDirectory NEW | navigator.storage.getDirectory() |
Returns Promise<FileSystemDirectoryHandle> for origin's private file system root. | Modern Browsers |
| getFileHandle | dir.getFileHandle(name, options) |
Gets file handle. Options: create: true to create if missing. |
Modern Browsers |
| getDirectoryHandle | dir.getDirectoryHandle(name, options) |
Gets subdirectory handle. Options: create: true to create if missing. |
Modern Browsers |
| removeEntry | dir.removeEntry(name, options) |
Deletes file or directory. Options: recursive: true for directories. |
Modern Browsers |
| getFile | fileHandle.getFile() |
Returns Promise<File> object with file data. | Modern Browsers |
| createWritable | fileHandle.createWritable() |
Returns Promise<FileSystemWritableFileStream> for writing. | Modern Browsers |
Example: Origin Private File System usage
// Get root directory
const root = await navigator.storage.getDirectory();
// Create/get file
const fileHandle = await root.getFileHandle("data.txt", { "create": true });
// Write to file
const writable = await fileHandle.createWritable();
await writable.write("Hello, File System!");
await writable.write({ "type": "write", "data": "\nNew line" });
await writable.close();
// Read from file
const file = await fileHandle.getFile();
const contents = await file.text();
console.log(contents);
// Create directory
const dirHandle = await root.getDirectoryHandle("documents", { "create": true });
// Create file in subdirectory
const subFileHandle = await dirHandle.getFileHandle("note.txt", { "create": true });
// List directory contents
for await (const entry of root.values()) {
console.log(entry.kind, entry.name); // "file" or "directory"
}
// Delete file
await root.removeEntry("data.txt");
// Delete directory recursively
await root.removeEntry("documents", { "recursive": true });
Note: Origin Private File System is private to origin and not accessible to user. Good for app-specific data, cache, temporary files. Not for user documents - use File System Access API for that. Storage is persistent and survives browser restarts.
6. Storage Quota (navigator.storage.estimate)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| estimate | navigator.storage.estimate() |
Returns Promise with quota and usage info. Properties: quota, usage. |
Modern Browsers |
| persist | navigator.storage.persist() |
Requests persistent storage (won't be cleared under pressure). Returns Promise<boolean>. | Modern Browsers |
| persisted | navigator.storage.persisted() |
Checks if storage is persistent. Returns Promise<boolean>. | Modern Browsers |
Example: Checking storage quota
// Check storage quota and usage
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
const usage = estimate.usage; // Bytes used
const quota = estimate.quota; // Bytes available
const percentUsed = (usage / quota * 100).toFixed(2);
console.log(`Storage: ${formatBytes(usage)} / ${formatBytes(quota)}`);
console.log(`${percentUsed}% used`);
if (estimate.usageDetails) {
console.log("Breakdown:", estimate.usageDetails);
// { indexedDB: 1234, caches: 5678, ... }
}
return { usage, quota, percentUsed };
}
}
function formatBytes(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
}
// Request persistent storage
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
console.log(`Persistent storage: ${isPersisted}`);
return isPersisted;
}
return false;
}
// Check if storage is persistent
async function checkPersistence() {
if (navigator.storage && navigator.storage.persisted) {
const isPersisted = await navigator.storage.persisted();
console.log(`Storage persisted: ${isPersisted}`);
return isPersisted;
}
return false;
}
// Monitor storage before operations
async function monitorStorage() {
const before = await navigator.storage.estimate();
// Perform storage operations
await performHeavyStorageOperation();
const after = await navigator.storage.estimate();
const increased = after.usage - before.usage;
console.log(`Storage increased by: ${formatBytes(increased)}`);
}
Note: Quota varies by browser and available disk space (typically 10-50% of available disk).
persist() requires user interaction or installed PWA. Persistent storage won't be cleared under storage pressure. Usage includes IndexedDB, Cache API, and File System API data.
Storage API Best Practices
- Use localStorage for small, simple data (settings, preferences) - synchronous and limited to 5-10MB
- Use IndexedDB for large structured data (offline databases, cached content) - asynchronous and scalable
- Use Cache API with Service Workers for offline-first applications and resource caching
- Always wrap storage operations in try/catch - quota exceeded and private mode can cause errors
- Use
JSON.stringify/parsefor complex objects in localStorage - Set appropriate cookie attributes:
Secure,HttpOnly,SameSite - Request persistent storage for critical data with
navigator.storage.persist() - Monitor quota usage with
navigator.storage.estimate()before large operations - Never store sensitive data (passwords, tokens) in client-side storage - vulnerable to XSS
- Consider data expiration strategies for cached content to prevent stale data