Chrome Extension Manifest V3 Migration Guide — Manifest V3 Guide
5 min readManifest V3 Migration Guide
Quick Checklist
- Change
"manifest_version": 2→3 - Replace
"background": { "scripts": [...] }→"background": { "service_worker": "background.js" } - Replace
"browser_action"/"page_action"→"action" - Move host permissions from
"permissions"→"host_permissions" - Replace
chrome.browserAction/chrome.pageAction→chrome.action - Replace
chrome.webRequestblocking →chrome.declarativeNetRequest - Update CSP: no
unsafe-eval, no remote scripts - Replace
chrome.tabs.executeScript→chrome.scripting.executeScript - Replace
chrome.tabs.insertCSS→chrome.scripting.insertCSS - Handle service worker lifecycle (no persistent state)
Manifest Changes
// MV2
{
"manifest_version": 2,
"background": { "scripts": ["bg.js"], "persistent": false },
"browser_action": { "default_popup": "popup.html" },
"permissions": ["tabs", "https://api.example.com/*"]
}
// MV3
{
"manifest_version": 3,
"background": { "service_worker": "background.js" },
"action": { "default_popup": "popup.html" },
"permissions": ["tabs"],
"host_permissions": ["https://api.example.com/*"]
}
Background Script → Service Worker
No DOM Access
// MV2 (had DOM in background page)
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// MV3 — use offscreen document
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML'
});
No Persistent State
// MV2
let count = 0;
chrome.browserAction.onClicked.addListener(() => count++);
// MV3 — use storage
import { createStorage, defineSchema } from '@theluckystrike/webext-storage';
const storage = createStorage(defineSchema({ count: 'number' }), 'local');
chrome.action.onClicked.addListener(async () => {
const c = (await storage.get('count')) || 0;
await storage.set('count', c + 1);
});
No setInterval/setTimeout (long-running)
// MV2
setInterval(() => poll(), 60000);
// MV3 — use alarms
chrome.alarms.create('poll', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener(a => { if (a.name === 'poll') poll(); });
Web Request → Declarative Net Request
// MV2 (blocking webRequest)
chrome.webRequest.onBeforeRequest.addListener(
() => ({ cancel: true }),
{ urls: ['*://ads.example.com/*'] },
['blocking']
);
// MV3 (declarativeNetRequest rules)
// rules.json:
[{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": { "urlFilter": "||ads.example.com", "resourceTypes": ["script", "image"] }
}]
Scripting API Changes
// MV2
chrome.tabs.executeScript(tabId, { code: 'alert("hi")' });
chrome.tabs.executeScript(tabId, { file: 'content.js' });
// MV3
await chrome.scripting.executeScript({
target: { tabId },
func: () => alert('hi')
});
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
Action API
// MV2
chrome.browserAction.setIcon({ path: 'icon.png' });
chrome.browserAction.onClicked.addListener(handler);
// MV3
chrome.action.setIcon({ path: 'icon.png' });
chrome.action.onClicked.addListener(handler);
Content Security Policy
// MV2 — allowed remote scripts
{ "content_security_policy": "script-src 'self' https://cdn.example.com; object-src 'self'" }
// MV3 — no remote scripts, no eval
{ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" } }
Messaging with @theluckystrike/webext-messaging
// Works the same in MV2 and MV3
import { createMessenger } from '@theluckystrike/webext-messaging';
type Msgs = { ACTION: { request: { data: string }; response: { ok: boolean } } };
const m = createMessenger<Msgs>();
// Handles SW lifecycle automatically
Common Migration Pitfalls
- Listeners inside async functions — lost on SW restart
- Global variables — reset on termination
- DOM APIs in service worker — use offscreen document
XMLHttpRequest— usefetch()insteadlocalStorage— usechrome.storagewindowobject — not available in SW
Cross-References
docs/guides/mv2-to-mv3-migration.mddocs/mv3/service-workers.mddocs/mv3/event-driven-architecture.mddocs/mv3/declarative-net-request.md-e —
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.