Storage API
Extension storage APIs and state management patterns including sync, local, and session storage
You are an expert in extension storage and state management for building browser extensions.
## Key Points
- Use `chrome.storage.sync` for user preferences that should follow the user across devices, and `chrome.storage.local` for larger data like caches or logs.
- Always provide default values when reading from storage to handle the case where keys have not been set yet.
- Batch related writes into a single `set()` call rather than making multiple calls, since each write has overhead and `sync` storage has rate limits.
- **Storing large binary data in `sync` storage** — the 100KB total and 8KB per-item limits are strict and silently enforced; large data belongs in `local` storage or IndexedDB.
- Exceeding `chrome.storage.sync` quotas (100 KB total, 8 KB per item) silently fails or throws — always keep synced data small and store large payloads in `local` instead.
- Treating `chrome.storage` like synchronous `localStorage` — all operations are asynchronous and must be awaited, or state may be stale or lost.
## Quick Example
```json
{
"permissions": ["storage"]
}
```skilldb get browser-extension-skills/Storage APIFull skill: 268 linesStorage API — Browser Extension Development
You are an expert in extension storage and state management for building browser extensions.
Overview
Browser extensions have access to dedicated storage APIs that are separate from the web page's localStorage or sessionStorage. The chrome.storage API provides asynchronous, area-based storage that can sync across devices, persist locally, or live only for the browser session.
Core Concepts
Storage Areas
There are three storage areas, each with different scoping and limits:
| Area | Persistence | Sync | Quota |
|---|---|---|---|
chrome.storage.local | Survives browser restarts | No | 10 MB (or unlimited with unlimitedStorage permission) |
chrome.storage.sync | Survives restarts, syncs across devices | Yes | 100 KB total, 8 KB per item, 1,800 writes/hour |
chrome.storage.session | Cleared on browser restart | No | 10 MB |
{
"permissions": ["storage"]
}
Basic CRUD Operations
// SET — store one or more key-value pairs
await chrome.storage.local.set({
userPrefs: { theme: 'dark', fontSize: 14 },
lastVisit: Date.now()
});
// GET — retrieve by key(s), with optional defaults
const { userPrefs = {}, lastVisit } = await chrome.storage.local.get({
userPrefs: {},
lastVisit: null
});
// GET ALL
const allData = await chrome.storage.local.get(null);
// REMOVE — delete specific keys
await chrome.storage.local.remove(['lastVisit']);
// CLEAR — remove everything in the area
await chrome.storage.local.clear();
Listening for Changes
chrome.storage.onChanged.addListener((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`[${areaName}] ${key}: ${JSON.stringify(oldValue)} -> ${JSON.stringify(newValue)}`);
}
});
// Area-specific listener (Chrome 114+)
chrome.storage.local.onChanged.addListener((changes) => {
if (changes.userPrefs) {
applyTheme(changes.userPrefs.newValue.theme);
}
});
Implementation Patterns
Settings Manager
A typed wrapper around storage for consistent access:
// settings.js
const DEFAULTS = {
theme: 'system',
notifications: true,
blockedSites: [],
syncInterval: 30
};
export const Settings = {
async getAll() {
const { settings = {} } = await chrome.storage.sync.get('settings');
return { ...DEFAULTS, ...settings };
},
async get(key) {
const all = await this.getAll();
return all[key];
},
async set(updates) {
const current = await this.getAll();
const merged = { ...current, ...updates };
await chrome.storage.sync.set({ settings: merged });
return merged;
},
async reset() {
await chrome.storage.sync.set({ settings: DEFAULTS });
return DEFAULTS;
},
onChange(callback) {
chrome.storage.sync.onChanged.addListener((changes) => {
if (changes.settings) {
callback(changes.settings.newValue, changes.settings.oldValue);
}
});
}
};
Cache with Expiration
// cache.js
export const Cache = {
async get(key) {
const { [`cache_${key}`]: entry } = await chrome.storage.local.get(`cache_${key}`);
if (!entry) return null;
if (entry.expiry && Date.now() > entry.expiry) {
await chrome.storage.local.remove(`cache_${key}`);
return null;
}
return entry.value;
},
async set(key, value, ttlMinutes = 60) {
const entry = {
value,
expiry: Date.now() + ttlMinutes * 60 * 1000,
storedAt: Date.now()
};
await chrome.storage.local.set({ [`cache_${key}`]: entry });
},
async invalidate(key) {
await chrome.storage.local.remove(`cache_${key}`);
},
async clearExpired() {
const all = await chrome.storage.local.get(null);
const expired = Object.keys(all).filter((k) =>
k.startsWith('cache_') && all[k].expiry && Date.now() > all[k].expiry
);
if (expired.length > 0) {
await chrome.storage.local.remove(expired);
}
}
};
Data Migration on Update
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
const { schemaVersion = 1 } = await chrome.storage.local.get('schemaVersion');
if (schemaVersion < 2) {
// Migrate from v1 to v2: rename 'prefs' to 'settings'
const { prefs } = await chrome.storage.local.get('prefs');
if (prefs) {
await chrome.storage.local.set({ settings: prefs });
await chrome.storage.local.remove('prefs');
}
}
if (schemaVersion < 3) {
// Migrate from v2 to v3: add new default field
const { settings = {} } = await chrome.storage.local.get('settings');
if (!settings.language) {
settings.language = 'en';
await chrome.storage.local.set({ settings });
}
}
await chrome.storage.local.set({ schemaVersion: 3 });
}
});
Reactive State with Storage Events
// Shared reactive state across popup, options, and content scripts
class StorageBackedState {
constructor(area, key, defaults) {
this.area = chrome.storage[area];
this.key = key;
this.defaults = defaults;
this.listeners = new Set();
this.area.onChanged.addListener((changes) => {
if (changes[this.key]) {
const newState = { ...this.defaults, ...changes[this.key].newValue };
this.listeners.forEach((fn) => fn(newState));
}
});
}
async get() {
const result = await this.area.get(this.key);
return { ...this.defaults, ...(result[this.key] || {}) };
}
async update(partial) {
const current = await this.get();
const next = { ...current, ...partial };
await this.area.set({ [this.key]: next });
return next;
}
subscribe(fn) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
}
// Usage
const appState = new StorageBackedState('local', 'appState', {
isEnabled: false,
count: 0,
filter: 'all'
});
Best Practices
- Use
chrome.storage.syncfor user preferences that should follow the user across devices, andchrome.storage.localfor larger data like caches or logs. - Always provide default values when reading from storage to handle the case where keys have not been set yet.
- Batch related writes into a single
set()call rather than making multiple calls, since each write has overhead andsyncstorage has rate limits.
Core Philosophy
Extension storage is the backbone of state management in a world where every context — service workers, popups, content scripts, options pages — is ephemeral. Treat chrome.storage as your single source of truth, not as a secondary cache. Every piece of state that matters should live in storage, and every context should read from it on initialization rather than assuming it has been passed the latest value through messaging.
Choose the right storage area for each piece of data based on its lifecycle and audience. User preferences that should follow the user across devices belong in sync. Large caches, logs, and temporary data belong in local. Ephemeral session state that should survive service worker restarts but not browser restarts belongs in session. Misusing areas — like putting a 50KB cache in sync — leads to silent quota failures and data loss.
Design for change from the beginning. Extensions evolve, and the shape of stored data changes with them. Implement a schema version number and migration logic in onInstalled so that updates transform old data structures into new ones without losing user data. Users who have been running your extension for years should not lose their settings because you renamed a key.
Anti-Patterns
-
Reading storage on every keystroke or scroll event —
chrome.storageis async and has I/O overhead; read state once on context initialization, cache it in memory, and useonChangedlisteners to stay up to date rather than polling. -
Storing large binary data in
syncstorage — the 100KB total and 8KB per-item limits are strict and silently enforced; large data belongs inlocalstorage or IndexedDB. -
Treating storage writes as synchronous — calling
chrome.storage.local.set()without awaiting it and immediately reading the value back may return stale data, especially under concurrent writes from multiple contexts. -
Not providing default values on reads — calling
chrome.storage.local.get('key')without a default means the first-ever read returnsundefined, which propagates through the UI as blank or broken state. -
Skipping data migration on extension updates — changing the shape of stored data (renaming keys, restructuring objects) without a migration step leaves existing users with orphaned data and broken functionality.
Common Pitfalls
- Exceeding
chrome.storage.syncquotas (100 KB total, 8 KB per item) silently fails or throws — always keep synced data small and store large payloads inlocalinstead. - Treating
chrome.storagelike synchronouslocalStorage— all operations are asynchronous and must be awaited, or state may be stale or lost.
Install this skill directly: skilldb add browser-extension-skills
Related Skills
Background Workers
Service workers and background scripts for event-driven extension logic, lifecycle management, and persistent tasks
Content Scripts
Content scripts for DOM manipulation, page interaction, and isolated world execution in browser extensions
Cross-Browser
Cross-browser extension compatibility across Chrome, Firefox, and Safari with API normalization and build strategies
Extension Publishing
Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation
Manifest V3
Chrome Manifest V3 fundamentals including configuration, permissions, and migration from V2
Messaging
Message passing between extension components including content scripts, service workers, popups, and external pages