Chrome Extension Extension State Persistence — Best Practices

6 min read

Extension 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:

Storage Options

chrome.storage.session

Ephemeral storage that survives service worker restarts but NOT browser restart. Ideal for:

// 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:

// 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:

// 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.