where synchronous access is acceptable and origin scoping is intentional.
browser.storage.sync requires aggressive payload minimization and rate-limit awareness.
IndexedDB is mandatory when data exceeds 10MB or requires indexing/filtering capabilities.
- Cross-context state consistency is only reliably achieved through
browser.storage.onChanged listeners, not direct DOM/window messaging.
Core Solution
1. Default Persistence: browser.storage.local
The standard choice for extension-specific data that must survive sessions. Provides a clean, async API consistent across browsers.
// Store data
await browser.storage.local.set({
weatherData: { temp: 72, city: 'San Francisco' },
lastUpdated: Date.now()
});
// Read data
const { weatherData, lastUpdated } = await browser.storage.local.get([
'weatherData',
'lastUpdated'
]);
// Delete specific key
await browser.storage.local.remove('weatherData');
// Clear everything
await browser.storage.local.clear();
2. Origin-Bound Caching: localStorage
Reserved for quick, synchronous key-value operations within extension UI pages. Not accessible from content scripts or MV3 service workers.
// Synchronous β no await needed
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
3. Cross-Device Preferences: browser.storage.sync
Ideal for small user settings that must follow the user across Firefox installations. Strict limits require careful payload management.
// User preferences that should follow them across devices
await browser.storage.sync.set({
searchEngine: 'duckduckgo',
temperatureUnit: 'celsius',
timeFormat: '24h'
});
const prefs = await browser.storage.sync.get([
'searchEngine',
'temperatureUnit',
'timeFormat'
]);
4. Large/Complex Data: IndexedDB
Required for historical data, search logs, or structured datasets needing queries. Async but verbose; wrap in promises for cleaner flow.
const db = await new Promise((resolve, reject) => {
const req = indexedDB.open('weather-extension', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
db.createObjectStore('forecasts', { keyPath: 'date' });
};
req.onsuccess = (e) => resolve(e.target.result);
req.onerror = (e) => reject(e.target.error);
});
// Store forecast data
const tx = db.transaction('forecasts', 'readwrite');
tx.objectStore('forecasts').put({ date: '2024-01-15', data: forecastData });
5. Production Pattern: Weather & Clock Dashboard
Real-world implementation separating persistent preferences from origin-bound cache:
// Persistent user preferences β browser.storage.local
const PREFS_DEFAULTS = {
city: '',
temperatureUnit: 'fahrenheit',
timeFormat: '12h',
theme: 'system',
worldClocks: [
{ label: 'London', timezone: 'Europe/London' },
{ label: 'Tokyo', timezone: 'Asia/Tokyo' }
]
};
async function getPrefs() {
const stored = await browser.storage.local.get(Object.keys(PREFS_DEFAULTS));
return { ...PREFS_DEFAULTS, ...stored };
}
async function setPrefs(updates) {
await browser.storage.local.set(updates);
}
// Weather cache β localStorage (same origin as newtab.html)
function getCachedWeather() {
const raw = localStorage.getItem('weatherCache');
if (!raw) return null;
const { data, timestamp } = JSON.parse(raw);
const age = Date.now() - timestamp;
return age < 3600000 ? { data, age } : null; // 1 hour TTL
}
function setCachedWeather(data) {
localStorage.setItem('weatherCache', JSON.stringify({
data,
timestamp: Date.now()
}));
}
6. Cross-Context Synchronization
Listen to storage mutations to keep options, UI, and background contexts aligned without manual messaging bridges.
// React to storage changes from any part of the extension
browser.storage.onChanged.addListener((changes, area) => {
if (area === 'local') {
if (changes.theme) {
applyTheme(changes.theme.newValue);
}
if (changes.city) {
refreshWeather(changes.city.newValue);
}
}
});
7. Quota Monitoring
Prevent silent failures by tracking usage against the 10MB limit.
async function checkStorageQuota() {
const usage = await browser.storage.local.getBytesInUse(null);
console.log(`Storage used: ${(usage / 1024).toFixed(1)} KB`);
// browser.storage.local.QUOTA_BYTES = 10485760 (10MB)
const pct = (usage / browser.storage.local.QUOTA_BYTES) * 100;
if (pct > 80) {
console.warn('Storage > 80% full');
}
}
Pitfall Guide
- MV3 Service Worker localStorage Blind Spot:
localStorage is explicitly unavailable in MV3 service workers/background scripts. Attempting to access it throws a ReferenceError or fails silently, breaking state persistence. Always route background data through browser.storage.local or IndexedDB.
- Sync Quota Throttling & Item Limits:
browser.storage.sync enforces 100KB total, 8KB per item, 512 items, and 1800 writes/hour. Exceeding these limits triggers QUOTA_BYTES or WRITE_RATE_LIMIT errors. Batch updates and compress payloads to avoid silent sync failures.
- Synchronous Main-Thread Blocking:
localStorage operations execute synchronously. Storing large JSON objects in popup or options pages blocks the event loop, causing UI jank and ANR states. Reserve localStorage only for small, critical key-value pairs.
- Cross-Context Data Fragmentation: Content scripts, background pages, and UI pages run in different origins/contexts. Data stored in one context's
localStorage is invisible to others, breaking extension-wide state. Use browser.storage APIs for shared state and browser.storage.onChanged for synchronization.
- Silent Quota Exhaustion: Failing to monitor
browser.storage.local.getBytesInUse() leads to unexpected QUOTA_EXCEEDED_ERR when users accumulate data. Implement proactive quota checks and graceful degradation (e.g., cache eviction, user warnings) before hitting limits.
- Missing Storage Change Listeners: Without
browser.storage.onChanged, UI pages, options, and background scripts drift out of sync, requiring manual refresh or complex runtime.sendMessage bridges. Always register listeners in persistent contexts to maintain reactive state.
- IndexedDB Transaction Leaks: Forgetting to properly close or handle
IDBTransaction errors causes database locks and memory leaks. Always wrap transactions in promises, handle onerror/onabort, and avoid long-running readwrite transactions that block concurrent access.
Deliverables
- π Extension Storage Architecture Blueprint: A decision-tree diagram mapping data types (preferences, cache, historical logs, transient state) to optimal storage APIs, including context boundaries (MV3 service worker vs UI vs content script), quota thresholds, and synchronization strategies.
- β
Storage Pre-Flight Checklist: