cross-browser-compatibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cross-Browser Extension Compatibility

跨浏览器扩展兼容性

Comprehensive guide to writing browser extensions that work across Chrome, Firefox, Safari, and Edge with proper feature detection and polyfills.
本文是一份编写可在Chrome、Firefox、Safari和Edge中运行的浏览器扩展的综合指南,包含正确的特性检测和Polyfill使用方法。

Overview

概述

Browser extensions share a common WebExtensions API standard, but implementations differ significantly. This skill covers how to handle those differences.
This skill covers:
  • API compatibility matrices
  • Polyfill usage and patterns
  • Feature detection techniques
  • Browser-specific workarounds
  • Manifest differences
This skill does NOT cover:
  • General JavaScript compatibility (use caniuse.com)
  • Extension store submission (see
    extension-anti-patterns
    skill)
  • UI framework differences
浏览器扩展遵循通用的WebExtensions API标准,但各浏览器的实现差异显著。本技能将介绍如何处理这些差异。
本技能涵盖:
  • API兼容性矩阵
  • Polyfill的使用与模式
  • 特性检测技巧
  • 浏览器特定的解决方案
  • Manifest差异
本技能不涵盖:
  • 通用JavaScript兼容性(可参考caniuse.com)
  • 扩展商店提交流程(请查看
    extension-anti-patterns
    技能)
  • UI框架差异

Quick Reference

快速参考

Browser API Namespaces

浏览器API命名空间

BrowserNamespacePromisesPolyfill Needed
Chrome
chrome.*
CallbacksYes
Firefox
browser.*
NativeNo
Safari
browser.*
NativeNo
Edge
chrome.*
CallbacksYes
浏览器命名空间Promise支持是否需要Polyfill
Chrome
chrome.*
回调式
Firefox
browser.*
原生支持
Safari
browser.*
原生支持
Edge
chrome.*
回调式

Universal Pattern

通用模式

typescript
// Use webextension-polyfill for consistent API
import browser from 'webextension-polyfill';

// Now works in all browsers with Promises
const tabs = await browser.tabs.query({ active: true });
typescript
// Use webextension-polyfill for consistent API
import browser from 'webextension-polyfill';

// Now works in all browsers with Promises
const tabs = await browser.tabs.query({ active: true });

API Compatibility Matrix

API兼容性矩阵

Core APIs

核心API

APIChromeFirefoxSafariEdgeNotes
action.*
MV3 only
alarms.*
Standard
bookmarks.*
Safari: no support
browserAction.*
MV2MV2MV2Use
action
in MV3
commands.*
Safari: limited
contextMenus.*
Standard
cookies.*
Safari: restrictions
downloads.*
Safari: no support
history.*
Safari: no support
i18n.*
Standard
identity.*
Firefox: partial
idle.*
Safari: no support
management.*
Safari: no support
notifications.*
Safari: no support
permissions.*
Safari: limited
runtime.*
Standard
scripting.*
Safari: limited
storage.*
Standard
tabs.*
Safari: some limits
webNavigation.*
Safari: limited
webRequest.*
Safari: observe only
windows.*
Safari: limited
APIChromeFirefoxSafariEdge说明
action.*
仅支持MV3
alarms.*
标准API
bookmarks.*
Safari:不支持
browserAction.*
MV2MV2MV2MV3中请使用
action
commands.*
Safari:支持有限
contextMenus.*
标准API
cookies.*
Safari:存在限制
downloads.*
Safari:不支持
history.*
Safari:不支持
i18n.*
标准API
identity.*
Firefox:部分支持
idle.*
Safari:不支持
management.*
Safari:不支持
notifications.*
Safari:不支持
permissions.*
Safari:支持有限
runtime.*
标准API
scripting.*
Safari:支持有限
storage.*
标准API
tabs.*
Safari:存在部分限制
webNavigation.*
Safari:支持有限
webRequest.*
Safari:仅支持观测
windows.*
Safari:支持有限

Advanced APIs

高级API

APIChromeFirefoxSafariEdgeWorkaround
declarativeNetRequest
Use webRequest
offscreen
109+109+Content script
sidePanel
114+114+Use popup
storage.session
102+115+16.4+102+Use local + clear
userScripts
120+120+Content scripts
APIChromeFirefoxSafariEdge替代方案
declarativeNetRequest
使用webRequest
offscreen
109+109+内容脚本
sidePanel
114+114+使用弹窗
storage.session
102+115+16.4+102+使用local存储+手动清理
userScripts
120+120+内容脚本

Polyfill Setup

Polyfill配置

Using webextension-polyfill

使用webextension-polyfill

The Mozilla webextension-polyfill normalizes the Chrome callback-style API to Firefox's Promise-based API.
Mozilla的webextension-polyfill可将Chrome的回调式API统一为Firefox的Promise式API。

Installation

安装

bash
npm install webextension-polyfill
bash
npm install webextension-polyfill

TypeScript types

TypeScript类型定义

npm install -D @anthropic-ai/anthropic-sdk-types/webextension-polyfill
undefined
npm install -D @anthropic-ai/anthropic-sdk-types/webextension-polyfill
undefined

Usage in Background Script

后台脚本中的使用

typescript
// background.ts
import browser from 'webextension-polyfill';

browser.runtime.onMessage.addListener(async (message, sender) => {
  const tabs = await browser.tabs.query({ active: true, currentWindow: true });
  return { tabId: tabs[0]?.id };
});
typescript
// background.ts
import browser from 'webextension-polyfill';

browser.runtime.onMessage.addListener(async (message, sender) => {
  const tabs = await browser.tabs.query({ active: true, currentWindow: true });
  return { tabId: tabs[0]?.id };
});

Usage in Content Script

内容脚本中的使用

typescript
// content.ts
import browser from 'webextension-polyfill';

const response = await browser.runtime.sendMessage({ type: 'getData' });
console.log(response);
typescript
// content.ts
import browser from 'webextension-polyfill';

const response = await browser.runtime.sendMessage({ type: 'getData' });
console.log(response);

WXT Framework (Recommended)

WXT框架(推荐)

WXT provides built-in polyfill support:
typescript
// No import needed - browser is global
export default defineContentScript({
  matches: ['*://*.example.com/*'],
  main() {
    // browser.* works everywhere
    browser.runtime.sendMessage({ type: 'init' });
  },
});
WXT框架内置了Polyfill支持:
typescript
// No import needed - browser is global
export default defineContentScript({
  matches: ['*://*.example.com/*'],
  main() {
    // browser.* works everywhere
    browser.runtime.sendMessage({ type: 'init' });
  },
});

Feature Detection Patterns

特性检测模式

Check API Availability

检查API可用性

typescript
// Check if API exists
function hasAPI(api: string): boolean {
  const parts = api.split('.');
  let obj: any = typeof browser !== 'undefined' ? browser : chrome;

  for (const part of parts) {
    if (obj && typeof obj[part] !== 'undefined') {
      obj = obj[part];
    } else {
      return false;
    }
  }
  return true;
}

// Usage
if (hasAPI('sidePanel.open')) {
  browser.sidePanel.open({ windowId });
} else {
  // Fallback to popup
  browser.action.openPopup();
}
typescript
// Check if API exists
function hasAPI(api: string): boolean {
  const parts = api.split('.');
  let obj: any = typeof browser !== 'undefined' ? browser : chrome;

  for (const part of parts) {
    if (obj && typeof obj[part] !== 'undefined') {
      obj = obj[part];
    } else {
      return false;
    }
  }
  return true;
}

// Usage
if (hasAPI('sidePanel.open')) {
  browser.sidePanel.open({ windowId });
} else {
  // Fallback to popup
  browser.action.openPopup();
}

Runtime Browser Detection

运行时浏览器检测

typescript
// Detect browser at runtime
function getBrowser(): 'chrome' | 'firefox' | 'safari' | 'edge' | 'unknown' {
  const ua = navigator.userAgent;

  if (ua.includes('Firefox')) return 'firefox';
  if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
  if (ua.includes('Edg/')) return 'edge';
  if (ua.includes('Chrome')) return 'chrome';

  return 'unknown';
}

// Detect from extension APIs
function getBrowserFromAPIs(): 'chrome' | 'firefox' | 'safari' | 'edge' {
  if (typeof browser !== 'undefined') {
    // @anthropic-ai/anthropic-sdk-ts-expect-error - browser_specific_settings only in Firefox
    if (browser.runtime.getBrowserInfo) return 'firefox';
    return 'safari';
  }
  if (navigator.userAgent.includes('Edg/')) return 'edge';
  return 'chrome';
}
typescript
// Detect browser at runtime
function getBrowser(): 'chrome' | 'firefox' | 'safari' | 'edge' | 'unknown' {
  const ua = navigator.userAgent;

  if (ua.includes('Firefox')) return 'firefox';
  if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
  if (ua.includes('Edg/')) return 'edge';
  if (ua.includes('Chrome')) return 'chrome';

  return 'unknown';
}

// Detect from extension APIs
function getBrowserFromAPIs(): 'chrome' | 'firefox' | 'safari' | 'edge' {
  if (typeof browser !== 'undefined') {
    // @anthropic-ai/anthropic-sdk-ts-expect-error - browser_specific_settings only in Firefox
    if (browser.runtime.getBrowserInfo) return 'firefox';
    return 'safari';
  }
  if (navigator.userAgent.includes('Edg/')) return 'edge';
  return 'chrome';
}

Feature Flags Pattern

特性标志模式

typescript
// features.ts
export const FEATURES = {
  sidePanel: hasAPI('sidePanel'),
  offscreen: hasAPI('offscreen'),
  sessionStorage: hasAPI('storage.session'),
  userScripts: hasAPI('userScripts'),
  declarativeNetRequest: hasAPI('declarativeNetRequest'),
} as const;

// Usage
import { FEATURES } from './features';

if (FEATURES.sidePanel) {
  // Use side panel
} else {
  // Use popup alternative
}
typescript
// features.ts
export const FEATURES = {
  sidePanel: hasAPI('sidePanel'),
  offscreen: hasAPI('offscreen'),
  sessionStorage: hasAPI('storage.session'),
  userScripts: hasAPI('userScripts'),
  declarativeNetRequest: hasAPI('declarativeNetRequest'),
} as const;

// Usage
import { FEATURES } from './features';

if (FEATURES.sidePanel) {
  // Use side panel
} else {
  // Use popup alternative
}

Browser-Specific Patterns

浏览器特定模式

Firefox-Specific

Firefox专属配置

Gecko ID (Required)

Gecko ID(必填)

json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com",
      "strict_min_version": "109.0"
    }
  }
}
json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com",
      "strict_min_version": "109.0"
    }
  }
}

Data Collection Permissions (2025+)

数据收集权限(2025年起)

json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com",
      "data_collection_permissions": {
        "required": [],
        "optional": ["technicalAndInteraction"]
      }
    }
  }
}
json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com",
      "data_collection_permissions": {
        "required": [],
        "optional": ["technicalAndInteraction"]
      }
    }
  }
}

Firefox Android Support

Firefox Android支持

json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com"
    },
    "gecko_android": {
      "strict_min_version": "120.0"
    }
  }
}
json
{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com"
    },
    "gecko_android": {
      "strict_min_version": "120.0"
    }
  }
}

Safari-Specific

Safari专属配置

Privacy Manifest Requirement

隐私清单要求

Safari extensions require a host app with
PrivacyInfo.xcprivacy
:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
</dict>
</plist>
Safari扩展需要宿主应用包含
PrivacyInfo.xcprivacy
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
</dict>
</plist>

Safari Limitations Handling

Safari限制处理

typescript
// Safari doesn't support webRequest blocking
async function blockRequest(details: WebRequestDetails) {
  const browser = getBrowser();

  if (browser === 'safari') {
    // Use declarativeNetRequest instead
    await browser.declarativeNetRequest.updateDynamicRules({
      addRules: [{
        id: 1,
        action: { type: 'block' },
        condition: { urlFilter: details.url }
      }]
    });
  } else {
    // Use webRequestBlocking
    return { cancel: true };
  }
}
typescript
// Safari doesn't support webRequest blocking
async function blockRequest(details: WebRequestDetails) {
  const browser = getBrowser();

  if (browser === 'safari') {
    // Use declarativeNetRequest instead
    await browser.declarativeNetRequest.updateDynamicRules({
      addRules: [{
        id: 1,
        action: { type: 'block' },
        condition: { urlFilter: details.url }
      }]
    });
  } else {
    // Use webRequestBlocking
    return { cancel: true };
  }
}

Chrome-Specific

Chrome专属配置

Service Worker State Persistence

Service Worker状态持久化

typescript
// Chrome service workers terminate after ~5 minutes
// Always persist state to storage

// BAD: State lost on worker termination
let count = 0;

// GOOD: Persist to storage
const countStorage = storage.defineItem<number>('local:count', {
  defaultValue: 0
});

async function increment() {
  const count = await countStorage.getValue();
  await countStorage.setValue(count + 1);
}
typescript
// Chrome service workers terminate after ~5 minutes
// Always persist state to storage

// BAD: State lost on worker termination
let count = 0;

// GOOD: Persist to storage
const countStorage = storage.defineItem<number>('local:count', {
  defaultValue: 0
});

async function increment() {
  const count = await countStorage.getValue();
  await countStorage.setValue(count + 1);
}

Offscreen Documents (Chrome/Edge only)

离屏文档(仅Chrome/Edge支持)

typescript
// For DOM access in MV3 service worker
if (hasAPI('offscreen')) {
  await chrome.offscreen.createDocument({
    url: 'offscreen.html',
    reasons: ['DOM_PARSER'],
    justification: 'Parse HTML content'
  });
}
typescript
// For DOM access in MV3 service worker
if (hasAPI('offscreen')) {
  await chrome.offscreen.createDocument({
    url: 'offscreen.html',
    reasons: ['DOM_PARSER'],
    justification: 'Parse HTML content'
  });
}

Manifest Differences

Manifest差异

Cross-Browser Manifest Generation

跨浏览器Manifest生成

typescript
// wxt.config.ts
export default defineConfig({
  manifest: ({ browser }) => ({
    name: 'My Extension',
    version: '1.0.0',

    // Chrome/Edge
    ...(browser === 'chrome' && {
      minimum_chrome_version: '116',
    }),

    // Firefox
    ...(browser === 'firefox' && {
      browser_specific_settings: {
        gecko: {
          id: 'my-extension@example.com',
          strict_min_version: '109.0',
        },
      },
    }),

    // Different permissions per browser
    permissions: [
      'storage',
      'activeTab',
      ...(browser !== 'safari' ? ['notifications'] : []),
    ],
  }),
});
typescript
// wxt.config.ts
export default defineConfig({
  manifest: ({ browser }) => ({
    name: 'My Extension',
    version: '1.0.0',

    // Chrome/Edge
    ...(browser === 'chrome' && {
      minimum_chrome_version: '116',
    }),

    // Firefox
    ...(browser === 'firefox' && {
      browser_specific_settings: {
        gecko: {
          id: 'my-extension@example.com',
          strict_min_version: '109.0',
        },
      },
    }),

    // Different permissions per browser
    permissions: [
      'storage',
      'activeTab',
      ...(browser !== 'safari' ? ['notifications'] : []),
    ],
  }),
});

MV2 vs MV3 Differences

MV2与MV3的差异

FeatureMV2MV3
Background
background.scripts
background.service_worker
Remote codeAllowedForbidden
executeScript
Eval strings allowedFunctions only
Content securityRelaxed CSPStrict CSP
webRequestBlocking
SupportedUse DNR
特性MV2MV3
后台脚本
background.scripts
background.service_worker
远程代码允许禁止
executeScript
允许执行字符串代码仅支持函数
内容安全策略宽松CSP严格CSP
webRequestBlocking
支持使用DNR替代

Testing Cross-Browser

跨浏览器测试

Manual Testing Matrix

手动测试矩阵

markdown
| Feature | Chrome | Firefox | Safari | Edge | Notes |
|---------|--------|---------|--------|------|-------|
| Install | [ ] | [ ] | [ ] | [ ] | |
| Popup opens | [ ] | [ ] | [ ] | [ ] | |
| Content script | [ ] | [ ] | [ ] | [ ] | |
| Background messages | [ ] | [ ] | [ ] | [ ] | |
| Storage sync | [ ] | [ ] | [ ] | [ ] | |
markdown
| 功能 | Chrome | Firefox | Safari | Edge | 说明 |
|---------|--------|---------|--------|------|-------|
| 安装 | [ ] | [ ] | [ ] | [ ] | |
| 弹窗打开 | [ ] | [ ] | [ ] | [ ] | |
| 内容脚本 | [ ] | [ ] | [ ] | [ ] | |
| 后台消息通信 | [ ] | [ ] | [ ] | [ ] | |
| 存储同步 | [ ] | [ ] | [ ] | [ ] | |

Automated Testing

自动化测试

typescript
// tests/browser-compat.test.ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';

describe('cross-browser compatibility', () => {
  it('handles missing sidePanel API', async () => {
    // Simulate Safari (no sidePanel)
    delete (fakeBrowser as any).sidePanel;

    const result = await openUI();
    expect(result.method).toBe('popup');
  });

  it('handles missing notifications API', async () => {
    delete (fakeBrowser as any).notifications;

    const result = await notify('Test');
    expect(result.fallback).toBe('console');
  });
});
typescript
// tests/browser-compat.test.ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';

describe('cross-browser compatibility', () => {
  it('handles missing sidePanel API', async () => {
    // Simulate Safari (no sidePanel)
    delete (fakeBrowser as any).sidePanel;

    const result = await openUI();
    expect(result.method).toBe('popup');
  });

  it('handles missing notifications API', async () => {
    delete (fakeBrowser as any).notifications;

    const result = await notify('Test');
    expect(result.fallback).toBe('console');
  });
});

Common Compatibility Issues

常见兼容性问题

Issue: tabs.query Returns Different Results

问题:tabs.query返回结果不一致

Problem: Safari returns fewer tab properties.
Solution:
typescript
const tabs = await browser.tabs.query({ active: true });
const tab = tabs[0];

// Always check property existence
const url = tab?.url ?? 'unknown';
const favIconUrl = tab?.favIconUrl ?? '/default-icon.png';
问题: Safari返回的标签页属性更少。
解决方案:
typescript
const tabs = await browser.tabs.query({ active: true });
const tab = tabs[0];

// Always check property existence
const url = tab?.url ?? 'unknown';
const favIconUrl = tab?.favIconUrl ?? '/default-icon.png';

Issue: Storage Quota Differences

问题:存储配额差异

BrowserLocalSyncSession
Chrome10MB100KB10MB
FirefoxUnlimited100KB10MB
Safari10MB100KB10MB
Solution:
typescript
async function safeStore(key: string, data: unknown) {
  const size = new Blob([JSON.stringify(data)]).size;

  if (size > 100 * 1024 && storageArea === 'sync') {
    console.warn('Data too large for sync, using local');
    await browser.storage.local.set({ [key]: data });
  } else {
    await browser.storage[storageArea].set({ [key]: data });
  }
}
浏览器Local存储Sync存储Session存储
Chrome10MB100KB10MB
Firefox无限制100KB10MB
Safari10MB100KB10MB
解决方案:
typescript
async function safeStore(key: string, data: unknown) {
  const size = new Blob([JSON.stringify(data)]).size;

  if (size > 100 * 1024 && storageArea === 'sync') {
    console.warn('Data too large for sync, using local');
    await browser.storage.local.set({ [key]: data });
  } else {
    await browser.storage[storageArea].set({ [key]: data });
  }
}

Issue: webRequest Blocking Not Working

问题:webRequest拦截失效

Problem: Safari doesn't support blocking webRequests.
Solution: Use declarativeNetRequest for all browsers:
typescript
// Works in all browsers
await browser.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1],
  addRules: [{
    id: 1,
    priority: 1,
    action: { type: 'block' },
    condition: {
      urlFilter: '*://ads.example.com/*',
      resourceTypes: ['script', 'image']
    }
  }]
});
问题: Safari不支持webRequest拦截。
解决方案: 对所有浏览器使用declarativeNetRequest:
typescript
// Works in all browsers
await browser.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1],
  addRules: [{
    id: 1,
    priority: 1,
    action: { type: 'block' },
    condition: {
      urlFilter: '*://ads.example.com/*',
      resourceTypes: ['script', 'image']
    }
  }]
});

References

参考资料