← Назад к новостям

5 Manifest V3 Gotchas That Cost Me Way More Time Than They Should Have

I've lost more hours to Chrome extension development than I'd like to admit. Not because extensions are hard, exactly — it's that Manifest V3 changes a bunch of assumptions that feel completely reasonable until they quietly break your extension in production, usually a week after you shipped it and stopped thinking about it.

Here are the five that got me the worst, and how I'd fix them if I were starting over.

1. Your service worker is not a background page

Coming from Manifest V2, it's tempting to treat your service worker like the old persistent background page — just a script that runs and keeps its state in memory. It isn't. Chrome kills idle service workers aggressively (sometimes in under a minute), and when a new event comes in, it spins up a fresh instance. Any global variable you were relying on is gone.

// ❌ This "works" in dev and then silently breaks in production let userSettings = null; chrome.runtime.onInstalled.addListener(() => { userSettings = { theme: "dark" }; // gone the moment the SW sleeps }); 

The fix is boring but non-negotiable: anything that needs to survive between events goes into chrome.storage, not a variable.

chrome.runtime.onInstalled.addListener(() => { chrome.storage.local.set({ userSettings: { theme: "dark" } }); }); 

2. Listeners registered "later" just... don't fire

This one is sneakier. If you register an event listener inside a promise, a callback, or after an await, it may never fire — because by the time that code runs, Chrome has already decided nothing is listening and moved on.

// ❌ Registered too late — event may already have fired and been missed chrome.storage.local.get(["enabled"], ({ enabled }) => { chrome.action.onClicked.addListener(handleClick); }); 

Listeners need to be registered synchronously, at the top level of your script, every time it runs:

// ✅ chrome.action.onClicked.addListener(handleClick); async function handleClick(tab) { const { enabled } = await chrome.storage.local.get(["enabled"]); // ... } 

3. CSP will quietly reject your inline scripts

If you're copy-pasting popup HTML from an old tutorial or an old extension of your own, there's a decent chance it has an inline