ct) | ~15β30 lines | ~2β5 MB |
Key Findings:
- Firefox MV3 retains full blocking
webRequest capability, making it the only major browser that doesn't force ad-blockers/network tools into declarative workarounds.
- Service worker suspension reduces idle memory footprint by ~75%, but requires explicit state persistence strategies.
- Migration complexity for Firefox is significantly lower than Chrome due to backward-compatible API surface and retained blocking capabilities.
- CSP tightening is universal across MV3 implementations; dependency auditing is mandatory.
Core Solution
The migration architecture centers on three pillars: manifest restructuring, service worker lifecycle adaptation, and CSP compliance. Below are the exact implementation patterns required for a successful Firefox MV3 transition.
Version declaration
// MV2
{
"manifest_version": 2
}
// MV3
{
"manifest_version": 3
}
Action key
// MV2
{
"browser_action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
}
}
// MV3
{
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
}
}
Background scripts β service workers
// MV2
{
"background": {
"scripts": ["background.js"]
}
}
// MV3
{
"background": {
"service_worker": "background.js"
}
}
Host permissions moved out of permissions
// MV2
{
"permissions": ["https://api.example.com/*", "storage", "tabs"]
}
// MV3
{
"permissions": ["storage", "tabs"],
"host_permissions": ["https://api.example.com/*"]
}
Service Worker Lifecycle & State Persistence
The biggest behavioral change is background scripts becoming service workers. Service workers:
- Don't have access to the DOM
- Get terminated when idle (no persistent state in memory)
- Must use
self instead of window
// MV2 background script β this works
window.myGlobalState = {};
setInterval(() => console.log('still alive'), 1000);
// MV3 service worker β this breaks
// window is not defined in service worker scope
// State is lost when the service worker stops
// Instead, persist state to storage
self.addEventListener('message', async (event) => {
const { type, data } = event.data;
if (type === 'SAVE_STATE') {
await browser.storage.session.set({ state: data });
}
});
Keeping a service worker alive
For new tab extensions, this is less of an issue since the new tab page itself is a foreground page with a normal DOM context. But if you need the background script:
// Use alarms to keep the service worker alive
browser.alarms.create('keepAlive', { periodInMinutes: 0.4 });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') {
// Service worker is alive
}
});
Content Security Policy Changes
MV3 tightened CSP:
// MV2 β this was allowed
{
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}
// MV3 β unsafe-eval is NOT allowed in extension pages
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
This breaks libraries that use eval() β notably some templating engines and older versions of Angular.
What Stayed the Same
For simple extensions like new tab pages, most things don't change:
chrome_url_overrides (or browser_url_overrides) still works
browser.storage API unchanged
browser.tabs API unchanged
- Content scripts work the same way
- The extension popup works the same way
Real-World Migration Example
Here's the before/after for Weather & Clock Dashboard:
// Before (MV2)
{
"manifest_version": 2,
"browser_action": {},
"permissions": ["storage"]
}
// After (MV3)
{
"manifest_version": 3,
"action": {},
"permissions": ["storage"]
}
For this extension (pure new tab page, no background script, no remote code), the migration was literally two line changes.
Pitfall Guide
- Assuming
window or DOM Access in Service Workers: Service workers execute in a dedicated worker context, not a window context. Accessing window, document, or DOM APIs will throw ReferenceError. Always use self and route UI interactions through message passing to content scripts or popups.
- Ignoring Service Worker Suspension: Firefox terminates idle service workers to save resources. Any in-memory variables, caches, or timers are wiped. Rely exclusively on
browser.storage.session or browser.storage.local for state that must survive suspension.
- Using
eval() or unsafe-eval in Dependencies: MV3 CSP strictly prohibits dynamic code execution. Libraries that compile templates at runtime or use eval() will fail silently or throw CSP violations. Pre-compile templates, use static bundling, or switch to CSP-compliant alternatives.
- Misplacing Host Permissions: Firefox separates host patterns from standard permissions. Placing URLs in the
permissions array instead of host_permissions results in silent authorization failures. Network requests to those hosts will be blocked without explicit error messages.
- Overusing Alarms to Keep SW Alive: While
browser.alarms with periodInMinutes: 0.4 prevents suspension, it forces periodic wake-ups that increase battery/CPU usage. Use this pattern only for extensions requiring continuous background polling, and implement graceful degradation when possible.
- Deprecated
browser_action/page_action Keys: Firefox MV3 unifies toolbar and page-specific buttons under the action key. Legacy keys trigger manifest validation warnings or parsing errors. Update all references to use the unified action API and handle context via browser.action vs browser.pageAction equivalents if needed.
Deliverables
π MV3 Migration Blueprint
- Architecture decision flowchart for background execution vs service worker lifecycle
- State persistence strategy matrix (session vs local vs in-memory)
- CSP compliance audit checklist for third-party dependencies
β
Pre-Launch Validation Checklist
βοΈ Configuration Templates
manifest.json MV3 baseline template with proper permission separation
- Service worker lifecycle boilerplate (message listener, storage fallback, alarm scheduler)
- CSP-compliant extension page configuration snippet
- Cross-browser compatibility wrapper for
browser vs chrome API namespaces