Chrome Extension Extension State Persistence — Best Practices
6 min readExtension State Persistence Patterns
Persisting state across MV3 service worker restarts is critical for building reliable Chrome extensions. Unlike the persistent background page in MV2, MV3 service workers are ephemeral and can be terminated after a period of inactivity, losing all in-memory state.
The Problem
Service workers in MV3 have a lifecycle that includes activation, idle, and termination phases. When a service worker goes idle and later gets terminated, all global variables and in-memory data are lost. This affects:
- Cached API responses
- User session data
- Temporary computation results
- Any state stored in global variables
Storage Options
chrome.storage.session
Ephemeral storage that survives service worker restarts but NOT browser restart. Ideal for:
- Temporary caches that can be rebuilt
- Session-specific data
- Inter-tab communication state
// Store ephemeral data that survives SW restart
await chrome.storage.session.set({ lastFetch: Date.now() });
chrome.storage.local
Persistent storage that survives browser restarts. Use for:
- User preferences
- Authentication tokens
- Any data that must persist across browser sessions
// Store persistent data
await chrome.storage.local.set({ userSettings: settings });
Choosing the Right Storage Area
| Use Case | Storage Area | Rationale |
|---|---|---|
| API response cache | session | Can be rebuilt, ephemeral |
| User preferences | local | Must persist across sessions |
| Auth tokens | local | Security critical, must persist |
| Temporary computation | session | Rebuildable state |
| Tab/window state | session | Survives SW restart, not browser crash |
State Hydration Pattern
Load state from storage when the service worker wakes up, and flush to storage before going to sleep.
// State manager with auto-persistence
class StateManager {
constructor(storageKey, defaultState = {}) {
this.storageKey = storageKey;
this.state = defaultState;
this.initialized = false;
}
async init() {
const stored = await chrome.storage.session.get(this.storageKey);
this.state = { ...this.state, ...stored[this.storageKey] };
this.initialized = true;
return this.state;
}
async set(key, value) {
this.state[key] = value;
await this.persist();
}
async persist() {
await chrome.storage.session.set({
[this.storageKey]: this.state
});
}
}
onSuspend Handler
Save critical state before the service worker terminates:
// Save state before SW terminates
chrome.runtime.onSuspend.addListener(async () => {
await chrome.storage.session.set({
cachedData: currentCache,
lastSync: Date.now()
});
});
Lazy Initialization
Reconstruct state from storage on first access rather than at startup:
let cachedUsers = null;
async function getUsers() {
if (!cachedUsers) {
const stored = await chrome.storage.session.get('users');
cachedUsers = stored.users || [];
}
return cachedUsers;
}
Global Variables Anti-Pattern
Never rely on global state in MV3 service workers:
// BAD - lost on SW restart
let globalCache = {};
async function handleMessage(msg) {
// globalCache is empty after SW restart
}
// GOOD - always read from storage
async function handleMessage(msg) {
const stored = await chrome.storage.session.get('cache');
const cache = stored.cache || {};
// use cache...
}
Caching Strategy: Hot Data in Memory + Storage Backing
Keep frequently accessed data in a global variable with storage as backup:
class HybridCache {
constructor(key) {
this.key = key;
this.memory = null;
}
async get() {
if (this.memory) return this.memory;
const stored = await chrome.storage.session.get(this.key);
this.memory = stored[this.key];
return this.memory;
}
async set(value) {
this.memory = value;
await chrome.storage.session.set({ [this.key]: value });
}
}
Promise-Based Initialization
Ensure state is loaded before processing messages:
let stateManager = null;
async function initState() {
stateManager = new StateManager('appState', { count: 0 });
return stateManager.init();
}
// Initialize on first message
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (!stateManager) {
initState().then(() => handleMessage(msg, sendResponse));
return true; // Keep channel open for async response
}
handleMessage(msg, sendResponse);
});
Session Storage for Tabs/Window State
Data stored in chrome.storage.session survives service worker restarts but not browser crash. This is useful for:
- Current tab state
- Window-specific data
- Inter-tab communication via storage events
// Track active tab across SW restarts
chrome.storage.session.set({ activeTabId: tab.id });
chrome.tabs.onActivated.addListener(async (activeInfo) => {
await chrome.storage.session.set({ activeTabId: activeInfo.tabId });
});
Cross-References
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.