` | Moderate | Full |
Key Findings:
- Firefox MV3 achieves Chrome-level memory optimization while preserving synchronous network interception capabilities.
- Service worker termination reduces idle overhead by ~60-75%, but requires explicit state persistence strategies.
- CSP hardening eliminates dynamic code execution vectors, forcing bundler-based workflows.
Core Solution
Migrating to Firefox MV3 requires systematic updates to background architecture, security policies, UI action APIs, and network interception patterns. Below are the precise implementation steps.
1. Background Script Migration: Persistent Pages β Service Workers
Service workers are event-driven and terminate when idle. In-memory state is lost on termination, requiring explicit persistence strategies.
MV2 (background page):
// manifest.json
{
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
MV3 (service worker):
// manifest.json
{
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
}
The key implication: service workers don't persist state in memory. They're event-driven and can be terminated at any time. Code like this breaks:
// β This DOESN'T work in MV3 service workers
let cachedData = {}; // Lost when service worker terminates!
chrome.tabs.onActivated.addListener(() => {
if (cachedData.weather) {
// cachedData might be {} if SW was terminated
}
});
Fix: Use browser.storage.session (MV3 only) or browser.storage.local:
// β
This works in MV3
async function getCachedData() {
const { cachedData } = await browser.storage.session.get('cachedData');
return cachedData || {};
}
async function setCachedData(data) {
await browser.storage.session.set({ cachedData: data });
}
2. Content Security Policy Hardening
MV3 eliminates dynamic script execution and remote code loading to prevent injection attacks.
MV2: More permissive CSP
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
MV3: Stricter β no unsafe-eval, no remote scripts
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
This means: no more eval(), no more loading scripts from external CDNs directly. You must bundle all JavaScript.
3. Action API Unification
Firefox MV3 consolidates browser_action and page_action into a single action namespace, simplifying UI management.
MV2: Separate browser_action and page_action
MV3: Unified action
// MV2
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
// MV3
"action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
In code:
// MV2
browser.browserAction.setBadgeText({ text: '5' });
// MV3
browser.action.setBadgeText({ text: '5' });
4. Network Interception Strategy
Firefox MV3 retains blocking webRequest, enabling synchronous request cancellation and modification. This is critical for privacy tools and ad-blockers.
// Firefox MV3 β still works!
browser.webRequest.onBeforeRequest.addListener(
(details) => {
if (details.url.includes('tracker.example.com')) {
return { cancel: true };
}
},
{ urls: ['<all_urls>'] },
['blocking']
);
This matters for ad blockers and privacy tools β uBlock Origin works on Firefox MV3 because of this.
5. Full Manifest Migration Example (New Tab Extension)
For new tab extensions like Weather & Clock Dashboard, the migration is mostly straightforward:
// Before (MV2)
{
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {},
"chrome_url_overrides": {
"newtab": "newtab.html"
}
}
// After (MV3)
{
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module" // Optional: enables ES modules
},
"action": {},
"chrome_url_overrides": {
"newtab": "newtab.html"
}
}
The newtab override itself doesn't change between MV2 and MV3 β that part is stable.
Pitfall Guide
- In-Memory State Assumption in Service Workers: Developers frequently assume variables persist across service worker lifecycles. Service workers are garbage-collected when idle. Always serialize transient state to
browser.storage.session or local before relying on it in event listeners.
- CSP Violations with
eval() or Dynamic Imports: MV3 strictly blocks unsafe-eval and remote script execution. Using eval(), new Function(), or loading CDN scripts will trigger runtime errors. Pre-bundle all dependencies and use static import statements.
- API Namespace Fragmentation (
browser_action vs action): Mixing legacy browser.browserAction.* calls with MV3 manifests causes TypeError: browser.browserAction is undefined. Standardize on browser.action.* and update all UI badge/popup logic.
- Cross-Browser
webRequest Dependency: Relying on blocking webRequest without feature detection breaks Chrome MV3 builds. Implement conditional logic or dual-rule engines (declarativeNetRequest for Chrome, webRequest for Firefox) to maintain cross-store compatibility.
- Ignoring Service Worker Startup Latency: Service workers require ~50-150ms to initialize on first event. Failing to handle async initialization gracefully causes race conditions in
onInstalled or onStartup listeners. Use self.addEventListener('install', ...) for pre-warming and defer non-critical setup.
- Misconfigured ES Module Background Scripts: Adding
"type": "module" without proper bundler configuration or MIME-type handling can cause SyntaxError: Cannot use import statement outside a module. Ensure your build pipeline outputs valid ES modules and verify Firefox's SW module support before deployment.
Deliverables
π¦ Firefox MV2 β MV3 Migration Blueprint
- Complete manifest diff matrix with Firefox-specific overrides
- Service worker state persistence patterns (
session vs local storage)
- CSP compliance checklist and bundler configuration templates (Webpack/Vite)
- Cross-browser compatibility matrix for
webRequest vs declarativeNetRequest
- UI action API migration guide (
browser_action β action)
β
Pre-Deployment Migration Checklist