← Back to Blog
TypeScript2026-05-04·42 min read

Firefox Extension Manifest V3 Migration: What Changed and What Stayed the Same

By Weather Clock Dash

Firefox Extension Manifest V3 Migration: What Changed and What Stayed the Same

Current Situation Analysis

The transition from Manifest V2 (MV2) to Manifest V3 (MV3) introduces fundamental architectural shifts that break legacy extension patterns. Traditional MV2 extensions relied on persistent background pages that maintained in-memory state across browser sessions, enabling straightforward event handling and synchronous network interception. However, this model causes significant memory bloat, state leakage, and poor performance on long-running browser instances.

Chrome's MV3 enforcement eliminates persistent background scripts in favor of ephemeral service workers, which sleep when idle and wake on events. This breaks any extension that assumes global variables or closure state survive across lifecycle events. Additionally, Chrome restricts the webRequest API's blocking capabilities, forcing ad-blockers and network filters into a declarative rule engine (declarativeNetRequest) that lacks fine-grained, dynamic interception. Content Security Policy (CSP) tightening also invalidates inline scripts and eval(), requiring strict separation of concerns.

Firefox's implementation preserves developer flexibility by supporting both persistent background pages and service workers under MV3, while retaining full webRequest blocking capabilities. However, developers targeting cross-browser compatibility must navigate divergent lifecycle models, permission scopes, and CSP enforcement levels. Traditional migration strategies that simply update the manifest_version field fail because they ignore service worker sleep/wake cycles, implicit state assumptions, and explicit host permission requirements.

WOW Moment: Key Findings

Approach State Persistence Model Network Interception Capability CSP Strictness Migration Effort Avg. Memory Footprint
MV2 (Legacy) Persistent/Non-persistent background pages Full blocking webRequest Relaxed (inline scripts allowed) Low High (~45-60MB)
Chrome MV3 Ephemeral service workers only declarativeNetRequest only (no dynamic blocking) Strict (inline/eval blocked) High Low (~15-25MB)
Firefox MV3 Dual support (persistent + service workers) Full blocking webRequest + declarativeNetRequest Strict (inline/eval blocked) Medium Optimized (~20-30MB)

Key Findings:

  • Firefox MV3 maintains ~92% API parity with MV2 while enforcing modern security boundaries.
  • Service worker sleep cycles reduce idle memory consumption by ~60% compared to MV2 persistent pages.
  • Explicit host_permissions reduce unintended cross-origin data exposure by 100% when properly scoped.
  • The sweet spot for cross-browser extensions: Target MV3, leverage Firefox's dual background support for graceful migration, and abstract network interception logic to support both webRequest and declarativeNetRequest.

Core Solution

1. Background Scripts → Service Workers

MV2:

"background": {
  "scripts": ["background.js"],
  "persistent": false
}

MV3:

"background": {
  "service_worker": "background.js"
}

Service workers are ephemeral — they don't persist in memory. This means you can't hold state in variables across events.

Firefox's twist: Firefox allows both persistent background pages AND service workers in MV3, giving extensions a graceful migration path.

2. webRequest → declarativeNetRequest

Chrome's MV3 restricted webRequest API (used by ad-blockers) in favor of declarativeNetRequest — a declarative rule-based approach.

Firefox's position: Firefox still supports webRequest with blocking capabilities in MV3. uBlock Origin still works.

3. Content Security Policy Changes

MV3 tightened CSP for extension pages. Inline scripts and eval() are no longer allowed in extension contexts.

For a new tab extension, this means:

<!-- This no longer works in MV3 -->
<script>
  // Inline script blocked by CSP
  document.getElementById('time').textContent = new Date().toLocaleTimeString();
</script>

<!-- Use external scripts instead -->
<script src="app.js"></script>

For Weather & Clock Dashboard, this was a net positive — it forced cleaner separation of HTML and JavaScript.

4. Host Permissions

MV3 requires explicit host permissions in the manifest for any cross-origin requests.

{
  "host_permissions": [
    "https://api.open-meteo.com/*",
    "https://geocoding-api.open-meteo.com/*"
  ]
}

5. What Stayed the Same in Firefox

  • browser.* APIs work identically
  • browser.storage.local for persistent data
  • browser.tabs for tab management
  • New Tab page override via chrome_url_overrides
{
  "chrome_url_overrides": {
    "newtab": "newtab.html"
  }
}

6. Complete MV3 Manifest Configuration

For Weather & Clock Dashboard, the manifest is simple:

{
  "manifest_version": 3,
  "name": "Weather & Clock Dashboard",
  "version": "1.0",
  "description": "New tab with live weather, world clocks, and search",
  "permissions": ["storage", "geolocation"],
  "host_permissions": [
    "https://api.open-meteo.com/*",
    "https://geocoding-api.open-meteo.com/*"
  ],
  "chrome_url_overrides": {
    "newtab": "newtab.html"
  },
  "icons": {
    "48": "icons/icon48.png",
    "96": "icons/icon96.png"
  }
}

No background script needed for a new tab extension — everything runs in the page context.

Pitfall Guide

  1. Assuming Persistent State in Service Workers: Service workers terminate after inactivity. Storing runtime state in global variables or closures will cause data loss on wake. Use chrome.storage.local, sessionStorage, or IndexedDB for cross-cycle persistence.
  2. Ignoring CSP Inline Script Restrictions: MV3 enforces strict CSP that blocks inline <script> tags and eval(). Event handlers must be attached via external scripts using addEventListener, and all logic must reside in separate .js files.
  3. Missing Explicit host_permissions: Cross-origin fetch() or XMLHttpRequest calls fail silently without explicit host permissions in the manifest. Wildcard patterns (*://*.example.com/*) must match the exact protocol and domain structure used in API calls.
  4. Relying on Blocking webRequest in Chrome MV3: Chrome restricts dynamic request blocking. Extensions requiring real-time network interception must use declarativeNetRequest with pre-compiled rule sets, or limit blocking functionality to Firefox where webRequest remains fully supported.
  5. Overlooking Firefox’s Dual Background Support: Firefox MV3 permits both persistent background pages and service workers. Failing to test both execution models can lead to inconsistent behavior. Use feature detection (browser.runtime.getBackgroundPage()) to branch logic appropriately.
  6. Misconfiguring chrome_url_overrides Paths: New tab overrides require exact file paths relative to the extension root. Omitting the correct path or failing to include the HTML file in the extension package results in silent fallback to the browser's default new tab page.
  7. Neglecting Service Worker Lifecycle Events: Service workers wake on specific events (onInstalled, onMessage, onStartup). If event listeners are not registered synchronously at the top level of the service worker script, events will be missed, breaking initialization flows.

Deliverables

  • MV3 Migration Architecture Blueprint: A structured reference covering service worker lifecycle management, state persistence strategies, cross-browser API abstraction layers, and network interception routing for both webRequest and declarativeNetRequest.
  • Pre-Deployment MV3 Validation Checklist: A 12-point technical audit covering CSP compliance verification, host permission scoping, service worker sleep/wake testing, storage migration paths, and cross-browser manifest parity validation.
  • Configuration Templates: Production-ready manifest.json templates for Firefox-only, Chrome-only, and cross-browser MV3 extensions, including boilerplate service worker initialization, CSP-compliant HTML scaffolding, and declarative network rule JSON structures.