Learn/Advanced Topics

JSON in Browser APIs — IndexedDB, Service Workers & More

Beyond fetch and localStorage, browsers offer powerful APIs for storing, caching, and transferring JSON data. This guide covers IndexedDB for offline databases, Service Worker caching for PWAs, cross-origin messaging with postMessage, and storage patterns for Web Extensions.

Browser Storage Landscape

APICapacityData TypesAsyncBest For
localStorage~5 MBStrings onlyNo (blocking)Small key-value settings
sessionStorage~5 MBStrings onlyNo (blocking)Per-tab temporary data
IndexedDB50-80% of diskStructured objects, BlobsYesOffline databases, large datasets
Cache APIQuota-basedHTTP Response objectsYesCaching network responses
chrome.storage5 MB (local), 100 KB (sync)JSON objectsYesWeb 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 2
4
5 request.onupgradeneeded = (event) => {
6 const db = event.target.result;
7 const oldVersion = event.oldVersion;
8
9 if (oldVersion < 1) {
10 // Version 1: create users store
11 const userStore = db.createObjectStore('users', { keyPath: 'id' });
12 userStore.createIndex('email', 'email', { unique: true });
13 userStore.createIndex('plan', 'plan', { unique: false });
14 }
15
16 if (oldVersion < 2) {
17 // Version 2: add orders store
18 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 };
26
27 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-only
5 await tx.complete;
6}
7
8async 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}
18
19// Usage
20const 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});
28
29const 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

Stale-While-Revalidate Strategy
Cache JSON API responses in a Service Workerjavascript
1// sw.js — Service Worker
2
3const API_CACHE = 'api-cache-v1';
4const API_ROUTES = ['/api/users', '/api/products', '/api/config'];
5
6self.addEventListener('fetch', (event) => {
7 const url = new URL(event.request.url);
8
9 if (!API_ROUTES.some(route => url.pathname.startsWith(route))) {
10 return; // only cache API routes
11 }
12
13 // Stale-while-revalidate
14 event.respondWith(
15 caches.open(API_CACHE).then(async (cache) => {
16 const cached = await cache.match(event.request);
17
18 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 fallback
26
27 return cached || fetchPromise;
28 })
29 );
30});
31
32// Clean up old caches on activation
33self.addEventListener('activate', (event) => {
34 event.waitUntil(
35 caches.keys().then(keys =>
36 Promise.all(
37 keys
38 .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 origin
6);
7
8// Receiver (popup / iframe)
9window.addEventListener('message', (event) => {
10 // Always validate the origin
11 if (event.origin !== 'https://myapp.example.com') return;
12
13 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.js
2const worker = new Worker('json-worker.js');
3
4worker.postMessage({
5 action: 'transform',
6 data: largeJsonArray, // automatically cloned via structured clone
7});
8
9worker.onmessage = (event) => {
10 const result = event.data; // transformed JSON back on main thread
11 renderTable(result);
12};
13
14// json-worker.js
15self.onmessage = (event) => {
16 const { action, data } = event.data;
17
18 if (action === 'transform') {
19 const result = data
20 .filter(item => item.active)
21 .map(item => ({
22 id: item.id,
23 name: item.name.toUpperCase(),
24 total: item.price * item.quantity,
25 }));
26
27 self.postMessage(result);
28 }
29};

structuredClone vs JSON.parse(JSON.stringify())

FeatureJSON round-tripstructuredClone
Date objectsConverted to stringsPreserved as Date
RegExpLost (becomes {})Preserved
Map / SetLostPreserved
ArrayBuffer / BlobErrorPreserved
Circular referencesThrows errorHandled correctly
undefined valuesStrippedPreserved
FunctionsStrippedError (not cloneable)
PerformanceSlower (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);
3
4 try {
5 await navigator.clipboard.writeText(json);
6 console.log('JSON copied to clipboard');
7 } catch (err) {
8 // Fallback for older browsers or permission denied
9 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}
17
18async 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});
9
10// Retrieve settings
11const { preferences } = await chrome.storage.local.get('preferences');
12
13// 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});
17
18// 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);
7
8 console.log(`Storage: ${usedMB} MB / ${totalMB} MB (${percentUsed}%)`);
9 return { usage, quota, percentUsed: parseFloat(percentUsed) };
10 }
11 return null;
12}
13
14// 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}

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.