📅 Published
Developing Browser Extensions in 2026: Best Practices for Chrome, Firefox, and Edge
Building a browser extension that works across Chrome, Firefox, and Edge means navigating a platform where the shared WebExtensions specification creates a comforting illusion of portability that breaks apart the moment you touch anything beyond basic tab management. The manifest formats differ. The API surfaces overlap but do not match. The review processes operate on different timelines with different criteria. The debugging tools vary. And the Manifest V3 transition — covered in the MV3 guide on this site — has introduced additional divergence where the specification was supposed to converge.
This guide covers the practical workflow for developing, testing, and publishing extensions across all three browsers in 2026, based on maintaining extensions that target all of them. The page is part of the development section and connects to the browser extensions topic hub.
Project structure for cross-browser development
The most maintainable approach uses a single source tree with a build step that generates browser-specific packages. The core logic, content scripts, and UI components are shared. The manifest and browser-specific API calls are abstracted.
src/
background.js # Shared background logic
content.js # Shared content script
popup/ # Shared popup UI
manifest.chrome.json
manifest.firefox.json
lib/
browser-polyfill.js # WebExtensions polyfill
build/
chrome/ # Built Chrome package
firefox/ # Built Firefox package
Mozilla's webextension-polyfill provides a Promise-based API wrapper that smooths over the callback vs. Promise differences between Chrome and Firefox. Use it from the start — retrofitting it into an existing extension is tedious.
import browser from 'webextension-polyfill';
// Works on both Chrome and Firefox
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
Manifest differences
Chrome requires Manifest V3. Firefox supports both MV2 and MV3. Edge uses Chromium's engine and accepts Chrome MV3 manifests with minor differences.
The key differences in the MV3 manifest:
background: Chrome requires "service_worker". Firefox accepts both "service_worker" and "scripts" (event page). Use "service_worker" for Chrome and "scripts" for Firefox if you want to avoid service worker lifecycle issues on Firefox.
browser_specific_settings: Firefox requires this field for addon ID and minimum version. Chrome ignores it.
// Firefox-specific
"browser_specific_settings": {
"gecko": {
"id": "your-extension@example.com",
"strict_min_version": "109.0"
}
}
Permissions: Chrome requires "host_permissions" separate from "permissions". Firefox also supports this but the runtime behaviour differs — Chrome prompts for host permissions; Firefox grants them at install.
Development and debugging workflow
Chrome: Load unpacked extension from chrome://extensions with Developer Mode enabled. The service worker inspector is accessible from the extension card. Console logs, breakpoints, and network inspection work through the standard DevTools.
Firefox: Load temporary addon from about:debugging#/runtime/this-firefox. Background script inspection opens a dedicated DevTools window. Firefox's extension debugging is more ergonomic than Chrome's — the background inspector is easier to find and the error reporting is more descriptive.
Edge: Identical to Chrome's workflow (load unpacked from edge://extensions). Edge uses Chromium's extension infrastructure, so Chrome-targeted extensions typically work without modification.
Live reload during development
The standard development loop of "make change → reload extension → test" is slow. Use a build tool with watch mode:
# Using web-ext (Mozilla's CLI tool, works for Chrome too)
web-ext run --source-dir=./build/firefox --firefox-profile=dev-profile
web-ext watches for file changes and automatically reloads the extension. For Chrome, extensions like "Extension Reloader" provide similar functionality, or use a build tool that triggers chrome.runtime.reload() via the debugging protocol.
Content script patterns
Content scripts run in the context of web pages and are the primary mechanism for extensions that interact with page content. The injection patterns that work reliably across browsers:
Static injection (declared in manifest): Reliable, runs on every matching page, but cannot be conditionally injected.
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}]
Programmatic injection (from background script): More flexible, allows conditional injection, but requires the scripting permission.
await browser.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js'],
});
The run_at timing matters. document_idle (default) waits until the page is mostly loaded, which is reliable but means your extension cannot intercept early page events. document_start runs before the page's own scripts, which is necessary for extensions that need to modify page behaviour before it executes.
The trackerless magnets extension on this site documents a complete content script implementation that intercepts and modifies page elements — a transferable pattern for many extension types.
Testing across browsers
Automated testing of browser extensions requires browser-specific WebDriver configurations:
// Selenium example for Chrome
const options = new chrome.Options();
options.addArguments(`--load-extension=${path.resolve('./build/chrome')}`);
const driver = new Builder().forBrowser('chrome').setChromeOptions(options).build();
For Firefox, web-ext provides a programmatic API for running tests against a real browser instance. Cross-browser test suites should verify:
- Extension loads without errors on each browser
- Content scripts execute on target pages
- Background script/service worker processes messages correctly
- Popup UI renders and functions
- Storage operations persist across browser restarts
- Permission prompts appear correctly (Chrome-specific)
Manual testing remains necessary for UX-sensitive features — automated tests catch functional regressions but miss visual and interaction issues.
Publishing to extension stores
Chrome Web Store: Review times are typically 1–3 days for updates, longer for new extensions. Google's automated scanning flags certain API patterns (particularly around remote code loading and broad host permissions) that require manual justification. Prepare clear descriptions of why your extension needs the permissions it requests.
Firefox Add-ons (AMO): Review times vary from hours to weeks depending on queue depth. AMO reviews are more thorough than Chrome's, with human reviewers examining source code. Source code submission is required if your extension uses a build process — reviewers must be able to verify the published package matches the source.
Edge Add-ons: Edge accepts Chrome extensions with minimal modification. The review process is generally faster than Chrome. For Chromium-based extensions, publishing to Edge is low-effort and reaches a meaningful user base.
Maintain a CHANGELOG and a clear version numbering scheme. All three stores show version history to users, and consistent release notes build trust.