Porting Chrome Extensions to Firefox: Complete Cross-Browser Guide
The browser extension ecosystem has evolved significantly, and developers increasingly need to support multiple browsers to maximize their reach. With Firefox maintaining a significant user base and Mozilla’s Add-on Marketplace (AMO) offering excellent discoverability, porting your Chrome extension to Firefox is a strategic move that can double your potential audience without requiring a complete rewrite of your codebase.
This comprehensive guide walks you through the entire process of porting Chrome extensions to Firefox, covering API compatibility, manifest differences, testing strategies, and publishing best practices. Whether you’re maintaining a simple utility extension or a complex developer tool, this guide provides the practical knowledge you need to achieve cross-browser compatibility.
Understanding the WebExtensions Architecture
Both Chrome and Firefox support the WebExtensions API, a standardized framework for building browser extensions. This shared foundation means that the core of your extension—HTML, CSS, and JavaScript—can often work across both browsers with minimal modifications. However, understanding the nuances between the two implementations is crucial for a smooth porting process.
The Common Ground
The WebExtensions API was designed with cross-browser compatibility in mind. Mozilla worked closely with Google and other browser vendors to create a unified API surface that works consistently across Chromium-based browsers (Chrome, Edge, Brave) and Firefox. This standardization covers essential APIs including:
- Content scripts: Scripts that run in the context of web pages
- Background scripts: Service workers that handle events and maintain state
- Browser actions: Toolbar buttons and popups
- Storage API: Local and sync storage capabilities
- Messaging APIs: Communication between content and background scripts
- Permissions system: Granular control over extension capabilities
The good news is that if you built your extension following Chrome’s Manifest V3 specification, approximately 80-90% of your code will work directly in Firefox without modification. The remaining 10-20% typically involves API-specific quirks, deprecated functions, or browser-specific features that require targeted adjustments.
Key Differences to Anticipate
Despite the common WebExtensions foundation, several areas require attention during porting:
API Naming Conventions: While most APIs share the same functionality, some use different namespace names. Chrome uses chrome.* while Firefox supports both browser.* (preferred, returns Promises) and chrome.* (for backward compatibility).
Manifest Requirements: Firefox requires additional fields in your manifest file, including explicit content script matching and stricter validation.
Extension ID Handling: Firefox generates extension IDs differently than Chrome, which affects how you manage identity and storage.
Update Mechanisms: The update checking and auto-update processes differ between Chrome Web Store and Mozilla Add-on Marketplace (AMO).
Preparing Your Chrome Extension for Porting
Before beginning the porting process, ensure your Chrome extension is structured in a way that facilitates cross-browser development. This preparation work will save significant time and reduce potential issues during the migration.
Organizing for Cross-Browser Compatibility
The most effective approach to multi-browser support involves structuring your extension with shared and browser-specific code paths. Create a clear separation between core logic that works universally and browser-specific implementations:
/src
/shared # Cross-browser code (content scripts, core logic)
/background # Background service worker
/utils # Helper functions
/browser # Browser-specific adapters
/chrome.js # Chrome-specific implementations
/firefox.js # Firefox-specific implementations
This architecture allows you to import the appropriate browser adapter based on detection, keeping your core business logic clean and reusable. Many successful cross-browser extensions use this pattern to maintain single source codebases while supporting Chrome, Firefox, Edge, and even Safari.
Manifest File Migration
The manifest.json file requires careful attention when porting to Firefox. While Chrome and Firefox both support Manifest V3, there are specific differences in how each browser interprets and validates the manifest.
Here’s a typical Chrome Manifest V3 structure and the modifications needed for Firefox compatibility:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "A cross-browser extension",
"permissions": [
"storage",
"activeTab",
"scripting"
],
"host_permissions": [
"https://*.example.com/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
For Firefox, you’ll need to add the browser_specific_settings field:
{
"browser_specific_settings": {
"gecko": {
"id": "your-extension@example.com",
"strict_min_version": "109.0"
}
}
}
The strict_min_version parameter is critical—it specifies the minimum Firefox version your extension supports. Mozilla recommends setting this to a version that supports all the APIs you use, typically no older than two major releases. As of 2025, Firefox 109+ supports the majority of Manifest V3 features.
Handling API Differences
Several common Chrome APIs require adjustments for Firefox compatibility:
chrome.storage vs browser.storage: Firefox’s browser.storage returns Promises, while Chrome’s chrome.storage uses callbacks. Use a polyfill or wrapper to normalize this behavior:
// Universal storage wrapper
const storage = {
async get(keys) {
if (typeof browser !== 'undefined') {
return await browser.storage.local.get(keys);
}
return new Promise(resolve => {
chrome.storage.local.get(keys, resolve);
});
},
async set(items) {
if (typeof browser !== 'undefined') {
return await browser.storage.local.set(items);
}
return new Promise(resolve => {
chrome.storage.local.set(items, resolve);
});
}
};
chrome.runtime.getManifest(): This works identically in both browsers, returning the parsed manifest object.
chrome.runtime.lastError: In Firefox with the browser.* namespace, errors are handled through Promise rejection rather than this callback parameter.
Declarative Net Request: Firefox supports this API but with some differences in how rules are defined and managed. Review Mozilla’s documentation for any specific limitations.
Step-by-Step Porting Process
With preparation complete, follow this systematic approach to port your extension:
Step 1: Audit Your Extension’s API Usage
Create a comprehensive list of all Chrome APIs your extension uses. Check each against Mozilla’s WebExtensions compatibility documentation to identify potential issues. Pay special attention to:
- Deprecated APIs: Some Chrome-specific APIs don’t exist in Firefox
- Experimental APIs: Features not yet standardized may work differently
- Platform-specific features: APIs like
chrome.sidePanelorchrome.debuggermay have Firefox equivalents or require alternative implementations
Step 2: Create a Firefox-Specific Manifest
Copy your manifest.json to manifest.firefox.json and add the required Firefox-specific fields. Test this manifest using Firefox’s web-ext tool or the about:debugging page.
Step 3: Implement Browser Detection
Add runtime detection to load the appropriate code paths:
const isFirefox = typeof browser !== 'undefined' &&
navigator.userAgent.includes('Firefox');
const browserAPI = isFirefox ? browser : chrome;
This simple pattern allows your code to work seamlessly in both environments while taking advantage of Firefox’s Promise-based APIs when available.
Step 4: Handle Content Script Isolation
Firefox handles content script isolation differently than Chrome. Ensure your content scripts are fully self-contained and don’t rely on shared state from the background script. Use message passing for any necessary communication:
// Content script
browser.runtime.sendMessage({ action: "getData" })
.then(response => {
console.log('Data received:', response.data);
});
// Background script
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "getData") {
sendResponse({ data: fetchData() });
}
return true; // Keep channel open for async response
});
Step 5: Update Icons and Assets
Firefox requires PNG icons and has specific size requirements. Ensure you provide icons at 16, 32, 48, and 128 pixels. Firefox also supports SVG icons for scalable rendering.
Step 6: Test Extensively
Use Firefox Developer Edition and the web-ext tool for development testing:
# Install web-ext
npm install -g web-ext
# Run extension in Firefox
web-ext run
This command starts Firefox with your extension loaded, automatically reloading when you make changes to your source files.
Common Porting Challenges and Solutions
Challenge 1: Service Worker Persistence
Chrome service workers have different lifecycle management than Firefox background scripts. Firefox background scripts are more persistent and may continue running longer than Chrome’s service workers.
Solution: Implement robust state management that doesn’t rely on in-memory persistence. Save critical state to storage frequently and restore it on startup:
// Initialize on background script startup
async function initialize() {
const savedState = await storage.get('appState');
if (savedState.appState) {
Object.assign(appState, savedState.appState);
}
}
// Save state periodically
setInterval(() => {
storage.set({ appState });
}, 30000);
Challenge 2: Content Script Injection
Firefox’s content script injection can behave differently, particularly with dynamically loaded pages and single-page applications.
Solution: Use the run_at parameter in your manifest to control when content scripts execute, and consider using document.addEventListener('DOMContentLoaded') or MutationObservers for dynamically loaded content:
// In content script
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
}
function main() {
// Your initialization logic
observeDOM();
}
function observeDOM() {
const observer = new MutationObserver(() => {
// Handle DOM changes
});
observer.observe(document.body, { childList: true, subtree: true });
}
Challenge 3: Native Messaging
If your extension uses native messaging to communicate with external applications, the implementation differs significantly between Chrome and Firefox.
Solution: Create separate native messaging hosts for each browser, or use cross-platform frameworks like Native Messaging Wrapper that handle browser differences transparently.
Challenge 4: WebRequest API Limitations
Firefox’s declarativeNetRequest API has different limits and capabilities compared to Chrome.
Solution: Check Mozilla’s documentation for current limits and design your extension’s request modification rules accordingly. Firefox may have stricter limits on the number of rules you can declare.
Testing Your Ported Extension
Comprehensive testing is essential for ensuring a quality cross-browser experience. Implement a multi-layered testing strategy:
Local Development Testing
Use Firefox Developer Edition for development—it provides excellent extension debugging tools and early access to new WebExtensions features. The Browser Toolbox (accessible via about:debugging) provides Chrome-like developer tools specifically for extensions.
Automated Testing
Implement automated tests that verify cross-browser compatibility:
// Example: Feature detection wrapper
function getBrowserFeatures() {
return {
promises: typeof browser !== 'undefined',
manifestVersion: chrome.runtime.getManifest().manifest_version,
storageSync: typeof browser !== 'undefined' &&
!!browser.storage?.sync,
declarativeNetRequest: typeof chrome !== 'undefined' &&
!!chrome.declarativeNetRequest
};
}
User Acceptance Testing
Before publishing to AMO, recruit beta testers who use Firefox specifically. Their feedback will reveal real-world issues that development testing might miss.
Publishing to Mozilla Add-on Marketplace
Once your extension is thoroughly tested, publishing to AMO requires several specific steps:
Creating Your AMO Account
- Visit addons.mozilla.org
- Sign in with a Mozilla account or create one
- Complete developer verification (may require a small fee in some regions)
Submitting Your Extension
- Navigate to the Developer Hub
- Select “Submit an Add-on”
- Upload your extension (zip file containing manifest and files)
- Complete the submission form:
- Name and description
- Category selection
- Privacy policy (required for certain permissions)
- Review screenshots and icon
The Review Process
Mozilla reviews all submissions for security, privacy, and functionality. The review process typically takes 1-7 days for new submissions. Firefox’s review is generally less automated than Chrome’s, with human reviewers assessing your extension’s code quality and compliance with policies.
Handling Updates
When you release updates:
# Using web-ext to package for AMO
web-ext build --ignore-files README.md
This creates a signed package you can upload through the AMO developer dashboard. Firefox also supports automatic updates through AMO, similar to Chrome’s auto-update system.
Maintaining Cross-Browser Extensions
Successful cross-browser extensions require ongoing maintenance:
Monitor API Changes
Subscribe to Mozilla’s Add-on Blog and Chrome’s Extensions Blog to stay informed about API changes. Implement feature detection to gracefully handle deprecated or removed APIs.
Use Build Tools
Implement a build system that generates browser-specific bundles:
// Example: Build script snippet
const manifest = require('./src/manifest.json');
const browserSpecific = {
firefox: { ...manifest, browser_specific_settings: { gecko: { id: process.env.AMO_ID } } },
chrome: { ...manifest }
};
module.exports = environment => browserSpecific[environment];
Automate Testing
Set up CI/CD pipelines that test your extension in both Chrome and Firefox:
# Example: GitHub Actions workflow
- name: Test in Firefox
run: web-ext test --firefox-binary ${{ firefox-path }}
- name: Test in Chrome
run: npx playwright test --browser chromium
Conclusion
Porting your Chrome extension to Firefox is a rewarding process that can significantly expand your user base with relatively modest effort. The WebExtensions API provides excellent cross-browser compatibility, and with careful attention to the differences outlined in this guide, you can maintain a single codebase that serves both Chrome and Firefox users effectively.
The key to success lies in proper preparation, systematic testing, and ongoing maintenance. By following the patterns and practices described here, you’ll be well-equipped to create cross-browser extensions that perform reliably across the Firefox ecosystem.
Remember to leverage Firefox’s unique features where appropriate—Mozilla’s add-on ecosystem has passionate users who appreciate extensions that feel native to their browser. With your extension now available on both Chrome Web Store and Mozilla Add-on Marketplace, you’re positioned to reach the widest possible audience for your browser extension.