Building a Chrome Extension with Manifest V3
TIL that MV3's move from persistent background pages to event-driven service workers means you can't hold long-lived in-memory state the way old extension tutorials assume — it gets killed and restarted constantly.
The Problem #
Following an older Manifest V2 tutorial's pattern, I kept application state (a queue of extracted orders waiting to sync) as an in-memory variable in the background script. It worked for a few minutes at a time, then silently reset — the queue would just be empty again with no error.
Context #
The extension needed to buffer data extracted from the POS page and periodically sync it to the delivery API, which felt like a natural fit for a long-running background script holding a queue in memory.
What I Tried #
Assumed a bug in my sync logic was clearing the queue and added logging around every place I mutated it.
What Went Wrong #
The logs showed the queue was populated correctly and then the entire background script re-initialized from scratch, wiping the in-memory variable — because in MV3, the background script is a service worker, not a persistent page, and Chrome terminates and restarts it whenever it's been idle for a short window.
The Solution #
Moved the queue out of memory and into chrome.storage.local, which survives service worker restarts, and stopped assuming any background-script state would persist between events.
// MV3 background: don't trust in-memory state to survive
async function enqueueOrder(order) {
const { queue = [] } = await chrome.storage.local.get("queue");
queue.push(order);
await chrome.storage.local.set({ queue });
}Why It Works #
chrome.storage.local is durable across service worker lifecycle events by design, which is exactly the guarantee a plain JS variable in a service worker doesn't have. MV3's whole model assumes the background script is ephemeral and event-driven, not a persistent process you can lean on for in-memory state.
Lessons Learned #
Porting MV2 patterns to MV3 by just changing the manifest version number doesn't work — the background execution model is fundamentally different, and any code that assumed a persistent background page needs to be redesigned around a service worker's actual lifecycle.
What I Would Do Differently #
I'd read MV3's service worker lifecycle documentation before writing any background-script state logic, instead of porting an MV2 pattern and debugging the lifecycle difference the hard way.
Related Concepts #
MV3 service worker lifecycle, chrome.storage vs. in-memory state, event-driven background scripts.
Related content
Writing a Reliable Regex for Extracting Delivery Times
TIL that a regex tuned against a handful of examples ('DELIVERY TIME: 2:00 PM') broke on real data within a day because of extra whitespace and inconsistent AM/PM casing I hadn't accounted for.
Extracting Delivery Information from Dynamically Rendered HTML
TIL that a content script querying the DOM immediately on page load found nothing, because the POS's order details render client-side, after the content script's own 'document_idle' point had already passed.
Injecting Custom UI Into a Website You Don't Own
TIL that a naive injected panel kept getting wiped out by the host page's own re-renders — the fix was watching for that, not fighting it, and mounting into a container the host page has no reason to touch.