C.W.K.
Stream
Lesson 04 of 06 · published

chrome.storage — The State Layer That Survives Eviction

~12 min · chrome.storage, state, async, service-worker, json

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"If the service worker is a function that wakes and sleeps, chrome.storage is the memory it's writing onto. Lesson 4 is about making peace with the fact that everything important must live in storage."

Two Storage Areas: local and sync

chrome.storage has multiple sub-namespaces. The two you'll use most:

  • chrome.storage.local — per-machine, persists until the extension is uninstalled. About 10 MB quota in MV3. Promise-based async API. The default choice for ClipDeck's clips, visit counts, and similar accumulating data.
  • chrome.storage.sync — synced across the user's signed-in Chrome instances. About 100 KB total, with a per-item cap around 8 KB. Use for user preferences (theme, hotkey config), NOT for accumulating data.

Less common but worth knowing: chrome.storage.session (added 2023) survives worker eviction but dies with the browser session. chrome.storage.managed is read-only and populated by enterprise policy.

The Async API

All chrome.storage operations are async. As of MV3 they return Promises (older callback signatures still work in parallel). Six methods cover almost everything:

  • get(key | keys[] | undefined) — read one key, several keys, or everything
  • set(object) — merge the given object into storage (idempotent; unrelated keys untouched)
  • remove(key | keys[]) — delete keys
  • clear() — wipe everything in this storage area
  • getBytesInUse(key | keys[] | undefined) — quota check
  • onChanged.addListener(fn) — react to writes from any context

Calling set with an object merges keys; it does NOT replace the whole storage. So you can write { visitCount: 5 } without affecting { clips: [...] }. That makes the storage layer composable across features.

JSON-Only Values

chrome.storage serializes values to JSON. Practical consequences:

  • Plain objects, arrays, strings, numbers, booleans, null — all fine.
  • Date objects → converted to ISO strings on the way in; you get back a string on the way out. Lose the type.
  • Map, Set → become {} or empty arrays. Convert explicitly to Object.fromEntries(map) or [...set] before storing.
  • Functions, class instances with methods, circular references — silent corruption or outright failure.

Stick to plain JSON-compatible shapes. Convert exotic types to plain values explicitly on the way in, reconstruct them on the way out.

Listening for Changes

chrome.storage.onChanged fires on every write in every context — popup, side panel, service worker, content script. The popup can react instantly to a write the worker made, no message passing required:

This is the alternative to chrome.runtime.sendMessage for read-state updates. The popup subscribes to onChanged, the SW writes to storage, the popup re-renders. No round-trip, no "is the worker awake" question.

ClipDeck's First Storage Round-Trip

Lesson 6 will use storage for ClipDeck's visit counter. The skeleton looks like the second code block below. That's the read-do-write-read pattern in its purest form. Stateless at the module level. ClipDeck's clip list (Track 3 and beyond) follows the same shape — read array from storage, append, write back. Edit and delete (Track 7) follow the same shape — read, mutate, write.

Once you internalize this single pattern, the rest of ClipDeck is mostly variations on its theme.

chrome.storage is the layer where ClipDeck's identity lives across worker evictions, browser restarts, and Mac reboots. Everything important goes through here. Everything ephemeral stays in scope and dies on schedule.
For debugging: chrome://extensions → ClipDeck → 'Inspect views: service worker' → Application tab → Storage → Extension Storage → 'local'. Every key/value is visible in real-time and you can edit them manually. Combined with the onChanged listener pattern, this is the fastest path to debugging stuck-state bugs.

Code

chrome.storage.local — basic get/set/remove/quota·javascript
// Basic chrome.storage.local usage (run from SW DevTools console)

// Read one key with default
const { visitCount = 0 } = await chrome.storage.local.get("visitCount");

// Read several keys
const { foo, bar } = await chrome.storage.local.get(["foo", "bar"]);

// Read everything
const all = await chrome.storage.local.get();

// Write — merges into existing data
await chrome.storage.local.set({
  visitCount: visitCount + 1,
  lastVisitAt: new Date().toISOString(),
});

// Remove a key
await chrome.storage.local.remove("lastVisitAt");

// Quota check
const bytes = await chrome.storage.local.getBytesInUse();
console.log("[ClipDeck SW] storage bytes in use:", bytes);
ClipDeck visit-counter preview — SW writes, popup reads + subscribes·javascript
// background.js — increment a visit counter on every completed page load
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
  if (info.status !== "complete" || !tab.url) return;
  const { visitCount = 0 } = await chrome.storage.local.get("visitCount");
  await chrome.storage.local.set({ visitCount: visitCount + 1 });
});

// popup.js — render the current count, and re-render on every change
async function renderCount() {
  const { visitCount = 0 } = await chrome.storage.local.get("visitCount");
  document.getElementById("count").textContent = String(visitCount);
}

chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === "local" && "visitCount" in changes) renderCount();
});

renderCount();

External links

Exercise

Add "permissions": ["storage"] to clipdeck/manifest.json (storage is a declared permission). Reload. Open the SW DevTools (chrome://extensions → ClipDeck → Inspect views: service worker). In the Console, paste: await chrome.storage.local.set({ greeting: 'hello from ClipDeck SW', when: new Date().toISOString() }); console.log(await chrome.storage.local.get());. Then switch to the Application tab → Storage → Extension Storage → 'local'. You should see both keys. Manually edit the 'greeting' value in the table. Back in the Console, register chrome.storage.onChanged.addListener((changes, area) => console.log('[change]', area, changes)). Edit the value again from the table — does the listener fire? (It should, with the change details.)
Hint
If await fails in the DevTools console with 'await is only valid in async functions', wrap the line: (async () => { await chrome.storage.local.set({...}); console.log(await chrome.storage.local.get()); })(). Modern Chrome DevTools usually allow top-level await but the older flag-gated console doesn't. If onChanged doesn't fire when you edit from the Application tab, it usually means the listener registered in a previous SW invocation that has since been evicted — re-register in the current console session.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.