Introduction to browser extensions

Browser extensions power everything from ad blockers to accessibility tools and developer utilities. If you're building one, you'll quickly discover that a Chrome and a Firefox extension are similar — but not identical.

Both ecosystems are based on the WebExtensions standard, yet they differ in APIs, permissions, architecture, and messaging behavior. This guide explains the real differences, the browser-specific APIs, and how message passing works in each.

The Common Ground: WebExtensions

Google Chrome and Mozilla Firefox both implement the WebExtensions API, which was originally designed to allow cross-browser compatibility.

Core concepts shared by both:

  • manifest.json
  • Background scripts / service workers
  • Content scripts
  • Extension pages (devtools, popup, options)
  • Permissions system
  • Messaging and Storage APIs

In many cases, the same codebase works with minimal changes. But under the hood, important differences exist.

Manifest Differences (MV2 vs MV3)

Chrome

Chrome has moved to Manifest V3 (MV3) as the standard.

  • Background scripts replaced by service workers
  • Stronger security model
  • No persistent background pages
  • Declarative APIs favored over dynamic ones
  • Stricter remote code restrictions

Firefox

Firefox still allows persistent backgrounds in Manifest V2 (MV2) extensions, which many developers find easier. It supports:

  • Manifest V2 (still supported)
  • Manifest V3 (supported but not fully identical to Chrome)

Background Architecture

Chrome

  • Uses service workers
  • Non-persistent (can shut down anytime)
  • Event-driven
  • No direct DOM access
  • Must re-initialize state frequently

Firefox

  • Supports persistent background scripts (MV2)
  • Easier long-running tasks
  • More predictable lifecycle
  • MV3 service worker support exists but differs slightly

Browser API Differences

Chrome's API: Uses chrome.*

  • Callback-based (MV2)
  • Promise support added gradually in MV3
chrome.tabs.query({ active: true }, (tabs) => {
  console.log(tabs);
});

Firefox's API: Uses browser.*

  • Promise-based
  • Modern async style
browser.tabs.query({ active: true }).then((tabs) => {
  console.log(tabs);
});

Cross-Browser Compatibility Trick

Firefox provides a polyfill so Chrome-style code can work:

if (typeof browser === "undefined") {
  var browser = chrome;
}

Or use Mozilla's official WebExtension polyfill.

Messaging Flow in Extensions

Extensions consist of isolated environments:

  • Background script
  • Content script
  • Popup / UI pages
  • DevTools pages (optional)

These cannot directly access each other's variables — messaging is required. Both browsers isolate extension scripts from the webpage context.

Communication flow

Direct communication between webpage and extension requires:

  • window.postMessage
  • DOM events
  • Injected scripts

This behavior is consistent across Chrome and Firefox — but the message routing expectations differ in practice.

Firefox

In Firefox extensions, messaging often needs to follow the explicit chain:

None

Sending messages directly from the popup to the content script may fail or behave inconsistently unless the correct tab context and permissions are used. The background script acts as a reliable central hub for routing messages.

Chrome

As compared to Firefox, Chrome can be considered as more lenient.

None

A popup can frequently send messages directly to a content script (for example, using chrome.tabs.sendMessage) and it will still work as expected, provided the content script is injected in the active tab.

Final Thoughts

Chrome and Firefox extensions share a common foundation, but subtle architectural differences — especially around APIs, permissions, lifecycle, and messaging behavior — can significantly impact development.

If you understand:

  • Manifest differences
  • API styles (chrome vs browser)
  • Background lifecycle
  • Message flow patterns

…you can build robust cross-browser extensions with confidence.