Browser Storage Landscape
| API | Capacity | Data Types | Async | Best For |
|---|---|---|---|---|
| localStorage | ~5 MB | Strings only | No (blocking) | Small key-value settings |
| sessionStorage | ~5 MB | Strings only | No (blocking) | Per-tab temporary data |
| IndexedDB | 50-80% of disk | Structured objects, Blobs | Yes | Offline databases, large datasets |
| Cache API | Quota-based | HTTP Response objects | Yes | Caching network responses |
| chrome.storage | 5 MB (local), 100 KB (sync) | JSON objects | Yes | Web Extension settings |
IndexedDB: Offline JSON Database
Creating a Database and Object Store
Initialize IndexedDB with schema migrationjavascript
1function openDatabase() {2 return new Promise((resolve, reject) => {3 const request = indexedDB.open('myapp', 2); // version 245 request.onupgradeneeded = (event) => {6 const db = event.target.result;7 const oldVersion = event.oldVersion;89 if (oldVersion < 1) {10 // Version 1: create users store11 const userStore = db.createObjectStore('users', { keyPath: 'id' });12 userStore.createIndex('email', 'email', { unique: true });13 userStore.createIndex('plan', 'plan', { unique: false });14 }1516 if (oldVersion < 2) {17 // Version 2: add orders store18 const orderStore = db.createObjectStore('orders', {19 keyPath: 'id',20 autoIncrement: true,21 });22 orderStore.createIndex('userId', 'userId', { unique: false });23 orderStore.createIndex('createdAt', 'createdAt', { unique: false });24 }25 };2627 request.onsuccess = () => resolve(request.result);28 request.onerror = () => reject(request.error);29 });30}CRUD Operations
Store and query JSON objectsjavascript
1async function addUser(db, user) {2 const tx = db.transaction('users', 'readwrite');3 const store = tx.objectStore('users');4 store.put(user); // put = upsert, add = insert-only5 await tx.complete;6}78async function getUsersByPlan(db, plan) {9 return new Promise((resolve, reject) => {10 const tx = db.transaction('users', 'readonly');11 const store = tx.objectStore('users');12 const index = store.index('plan');13 const request = index.getAll(plan);14 request.onsuccess = () => resolve(request.result);15 request.onerror = () => reject(request.error);16 });17}1819// Usage20const db = await openDatabase();21await addUser(db, {22 id: 'user_1',23 name: 'Alice',24 email: '[email protected]',25 plan: 'pro',26 preferences: { theme: 'dark', language: 'en' },27});2829const proUsers = await getUsersByPlan(db, 'pro');Querying Nested Properties
IndexedDB indexes only work on top-level properties. To query nested fields like
preferences.theme, create a denormalized index field when storing: user.themeIndex = user.preferences.theme.Service Workers and Cache API
Cache JSON API responses in a Service Workerjavascript
1// sw.js — Service Worker23const API_CACHE = 'api-cache-v1';4const API_ROUTES = ['/api/users', '/api/products', '/api/config'];56self.addEventListener('fetch', (event) => {7 const url = new URL(event.request.url);89 if (!API_ROUTES.some(route => url.pathname.startsWith(route))) {10 return; // only cache API routes11 }1213 // Stale-while-revalidate14 event.respondWith(15 caches.open(API_CACHE).then(async (cache) => {16 const cached = await cache.match(event.request);1718 const fetchPromise = fetch(event.request)19 .then((response) => {20 if (response.ok) {21 cache.put(event.request, response.clone());22 }23 return response;24 })25 .catch(() => cached); // offline fallback2627 return cached || fetchPromise;28 })29 );30});3132// Clean up old caches on activation33self.addEventListener('activate', (event) => {34 event.waitUntil(35 caches.keys().then(keys =>36 Promise.all(37 keys38 .filter(key => key !== API_CACHE)39 .map(key => caches.delete(key))40 )41 )42 );43});postMessage and structuredClone
Cross-Origin Communication
Send JSON between windows safelyjavascript
1// Sender (parent window)2const popup = window.open('https://partner.example.com/widget');3popup.postMessage(4 { type: 'config', theme: 'dark', userId: 'user_1' },5 'https://partner.example.com' // always specify target origin6);78// Receiver (popup / iframe)9window.addEventListener('message', (event) => {10 // Always validate the origin11 if (event.origin !== 'https://myapp.example.com') return;1213 const data = event.data;14 if (data.type === 'config') {15 applyConfig(data);16 }17});Web Worker Communication
Process JSON in a Web Workerjavascript
1// main.js2const worker = new Worker('json-worker.js');34worker.postMessage({5 action: 'transform',6 data: largeJsonArray, // automatically cloned via structured clone7});89worker.onmessage = (event) => {10 const result = event.data; // transformed JSON back on main thread11 renderTable(result);12};1314// json-worker.js15self.onmessage = (event) => {16 const { action, data } = event.data;1718 if (action === 'transform') {19 const result = data20 .filter(item => item.active)21 .map(item => ({22 id: item.id,23 name: item.name.toUpperCase(),24 total: item.price * item.quantity,25 }));2627 self.postMessage(result);28 }29};structuredClone vs JSON.parse(JSON.stringify())
| Feature | JSON round-trip | structuredClone |
|---|---|---|
| Date objects | Converted to strings | Preserved as Date |
| RegExp | Lost (becomes {}) | Preserved |
| Map / Set | Lost | Preserved |
| ArrayBuffer / Blob | Error | Preserved |
| Circular references | Throws error | Handled correctly |
| undefined values | Stripped | Preserved |
| Functions | Stripped | Error (not cloneable) |
| Performance | Slower (string intermediary) | Faster (no string step) |
Clipboard API
Copy JSON to clipboard programmaticallyjavascript
1async function copyJsonToClipboard(data) {2 const json = JSON.stringify(data, null, 2);34 try {5 await navigator.clipboard.writeText(json);6 console.log('JSON copied to clipboard');7 } catch (err) {8 // Fallback for older browsers or permission denied9 const textarea = document.createElement('textarea');10 textarea.value = json;11 document.body.appendChild(textarea);12 textarea.select();13 document.execCommand('copy');14 document.body.removeChild(textarea);15 }16}1718async function readJsonFromClipboard() {19 const text = await navigator.clipboard.readText();20 try {21 return JSON.parse(text);22 } catch {23 throw new Error('Clipboard does not contain valid JSON');24 }25}Web Extensions: chrome.storage
Store and sync JSON in browser extensionsjavascript
1// Store settings locally (5 MB limit)2await chrome.storage.local.set({3 preferences: {4 theme: 'dark',5 fontSize: 14,6 enabledTools: ['validator', 'formatter', 'diff'],7 },8});910// Retrieve settings11const { preferences } = await chrome.storage.local.get('preferences');1213// Sync settings across devices (100 KB limit, 8 KB per item)14await chrome.storage.sync.set({15 shortcuts: { format: 'Ctrl+Shift+F', validate: 'Ctrl+Shift+V' },16});1718// Listen for changes (works across extension pages)19chrome.storage.onChanged.addListener((changes, areaName) => {20 if (areaName === 'local' && changes.preferences) {21 applyPreferences(changes.preferences.newValue);22 }23});Storage Quota Management
Check and manage storage quotajavascript
1async function checkStorageQuota() {2 if ('storage' in navigator && 'estimate' in navigator.storage) {3 const { usage, quota } = await navigator.storage.estimate();4 const usedMB = (usage / (1024 * 1024)).toFixed(2);5 const totalMB = (quota / (1024 * 1024)).toFixed(2);6 const percentUsed = ((usage / quota) * 100).toFixed(1);78 console.log(`Storage: ${usedMB} MB / ${totalMB} MB (${percentUsed}%)`);9 return { usage, quota, percentUsed: parseFloat(percentUsed) };10 }11 return null;12}1314// Request persistent storage (prevents eviction under pressure)15async function requestPersistentStorage() {16 if (navigator.storage?.persist) {17 const granted = await navigator.storage.persist();18 console.log(`Persistent storage: ${granted ? 'granted' : 'denied'}`);19 return granted;20 }21 return false;22}Try These Tools
Continue Learning
Frequently Asked Questions
When should I use IndexedDB instead of localStorage?
Use IndexedDB when you need to store structured JSON objects larger than 5 MB, query by indexes, handle transactions, or store binary data alongside JSON. localStorage is limited to ~5 MB of string key-value pairs and blocks the main thread on read/write. IndexedDB is asynchronous and can store hundreds of megabytes.
Can Service Workers cache JSON API responses?
Yes. Service Workers intercept fetch requests and can cache JSON responses using the Cache API. Common strategies include cache-first (fast, potentially stale), network-first (fresh, with offline fallback), and stale-while-revalidate (fast initial load, background refresh).
What is structuredClone and how does it relate to JSON?
structuredClone() is a browser API that deep-copies objects, including types JSON cannot represent (Date, RegExp, Map, Set, ArrayBuffer, Blob). Unlike JSON.parse(JSON.stringify(obj)), it handles circular references and preserves type fidelity. Use it for copying complex objects; use JSON serialization when you need a string.
Is postMessage safe for sending JSON between origins?
postMessage is safe if you always validate the origin of incoming messages using event.origin and never eval or trust message data blindly. Always specify the target origin when sending (avoid using "*" in production). The browser serializes message data using the structured clone algorithm.
How much data can IndexedDB store?
IndexedDB can typically store up to 50-80% of available disk space (varies by browser). Chrome allows up to 60% of disk, Firefox allows up to 50%. You can check available quota with navigator.storage.estimate(). Browsers may evict data under storage pressure unless you request persistent storage.
How do I send JSON data on page unload?
Use navigator.sendBeacon(url, jsonBlob). It is specifically designed for sending analytics or telemetry data when the page is closing. Unlike fetch, sendBeacon is guaranteed to be sent even if the page navigates away. Create a Blob with type "application/json" from your JSON string.