Skip to content
Published on

ブラウザ拡張機能開発 2026 — Manifest V3 / Plasmo / WXT / Side Panel API / Safari Web Extensions 徹底ガイド

Authors

プロローグ — MV3 はついに本物になった

ブラウザ拡張機能の開発者は、5 年間「Manifest V3 はもうすぐ強制されますよ」と言われ続けた。毎回延期された。2022 年 1 月。2023 年 1 月。2024 年 1 月。そして 2024 年 6 月、Chrome はついにその約束を守った。

2024 年 6 月以降、Chrome 安定版で MV2 拡張機能は無効化された。Chrome Web Store への MV2 新規登録は、その 1 年前にすでに塞がれていた。2025 年にはエンタープライズ向けの逃げ道(ExtensionManifestV2Availability ポリシー)も段階的に廃止され、2026 年 5 月時点で MV2 は事実上、博物館行きの過去のものだ。

この変化の重みは、単に「マニフェストのバージョンが上がった」だけではない。

  • バックグラウンドページはサービスワーカーになった。 常駐していたページが、イベントに応じて眠ったり目を覚ましたりするワーカーになった。
  • blocking webRequest は declarativeNetRequest に置き換わった。 uBlock Origin は Chrome から去り、代わりに uBlock Origin Lite が登場した。
  • ホスト権限はランタイムオプトインがデフォルトになった。 「すべてのサイトへのアクセス」はもはや、インストール時に自動承認されない。
  • Side Panel API・Offscreen Documents API・Scripting API・declarativeNetRequest — これらの新 API は、バックグラウンドページ時代には不可能だったことを可能にする。

同時に、拡張機能の開発ツールも進化した。Plasmo は React を一等市民として迎え入れ、WXT は Vite ベースで軽くて速い代替を持ち込んだ。vite-plugin-web-extension はもっとミニマルな道を提供する。本稿はその全体像を見ていく。


第 1 章 · 2026 年のブラウザ拡張機能 — MV3 時代の風景

まずは全体像から。

                    Browser Extension API
                            |
        +-------------------+-------------------+
        |                   |                   |
   Chrome (Blink)      Firefox (Gecko)     Safari (WebKit)
        |                   |                   |
   manifest_version: 3   manifest_version: 3   manifest_version: 3
   service_worker        background.scripts    background.scripts
                         (event page)          (persistent: false)
        |                   |                   |
   declarativeNetRequest   webRequest blocking  declarativeNetRequest
                           (MV2 互換を維持)      (限定的な webRequest)
        |                   |                   |
   chrome.* namespace     browser.* + chrome.*  browser.* (Promise)
   Side Panel API         sidebarAction         一部未サポート
   Offscreen Documents    backgroundScripts     一部未サポート

この 1 枚の図が、2026 年の拡張機能開発の本質を捉えている。

  • Chrome(および Edge・Brave・Opera のような Chromium 派生): MV3 のみ。blocking webRequest なし。
  • Firefox: MV3 をサポートしつつ、blocking webRequest を一部維持(特に広告ブロッカーのため)。バックグラウンドはイベントページモデル(休眠/起動)。
  • Safari: WebKit ベース。iOS/iPadOS でも拡張機能が動く(Safari 15+、iOS 15+)。一部 API は未サポート。

3 ブラウザは WebExtensions 標準(W3C Browser Extensions Community Group)を共有するが、細部で分岐する。「一度書いてどこでも動く」のスローガンは半分しか真実ではない。

拡張機能の 4 大コンポーネント

+----------------------------------------------------+
|         拡張機能パッケージ (.crx / .xpi / .pkg)       |
+----------------------------------------------------+
|  manifest.json  (メタデータ + 権限)                  |
|                                                     |
|  +-----------------+    +-----------------+        |
|  | Service Worker  |    |  Content Script |        |
|  | (background)    |<-->|  (ページに注入)  |        |
|  +-----------------+    +-----------------+        |
|         ^                       ^                   |
|         |                       |                   |
|         v                       v                   |
|  +-----------------+    +-----------------+        |
|  |   Popup / UI    |    |   Side Panel    |        |
|  | (browser_action)|    |  (Chrome 114+)  |        |
|  +-----------------+    +-----------------+        |
|                                                     |
|  Offscreen Documents (DOM が必要な作業に)            |
+----------------------------------------------------+

各コンポーネントは別々の実行コンテキスト。通信はメッセージパッシング(chrome.runtime.sendMessage 等)で行う。


第 2 章 · Manifest V3 — 2024 年 6 月の強制適用以降

最も基本的な manifest.json(Chrome MV3)

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A simple MV3 extension",
  "icons": {
    "16": "icons/16.png",
    "48": "icons/48.png",
    "128": "icons/128.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icons/16.png"
  },
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"]
    }
  ],
  "permissions": ["storage", "tabs", "sidePanel"],
  "host_permissions": ["https://api.example.com/*"],
  "side_panel": {
    "default_path": "sidepanel.html"
  }
}

MV2 から MV3 への主要な差分

項目MV2MV3
Background常駐ページ / イベントページService Worker(Firefox はイベントページ)
webRequest blocking可能declarativeNetRequest に置換(Firefox は一部維持)
ホスト権限インストール時に自動付与ランタイムオプトイン可能(activeTab/optional_host_permissions)
リモートコード許可禁止(すべての JS をパッケージに同梱)
Content Security Policyオブジェクト形式より厳格なオブジェクト形式
Action APIbrowser_action + page_action統合された action
Promise API一部のみすべての chrome.* API が Promise 対応

リモートコード禁止は大きな変化だ。広告ブロッカーがリモートのフィルタリストを動的に評価できなくなり、分析・実験ツールがクラウドから JS を取得して実行するパターンはすべて書き直しを迫られた。

CRX フォーマット — 拡張機能はどう梱包されるか

Chrome 拡張機能は .crx ファイルとして配布される。これは本質的に ヘッダ付きの ZIP ファイルだ。

[CRX3 Magic: "Cr24"] [Version: 3] [Header Length] [Header (ProtoBuf)]
  +- RSA 署名(開発者鍵)
  +- ECDSA 署名(Google またはセルフ)
[拡張機能ファイルの ZIP アーカイブ]

署名鍵は拡張機能の ID を決定する。同じ鍵で署名すれば同じ ID、別の鍵なら別の ID。Web Store に登録すれば、Google がその鍵を管理する。


第 3 章 · Service Worker — バックグラウンドページの後継

MV3 の核心は、バックグラウンドコンテキストのモデルが変わったことだ。

Service Worker のライフサイクル

[イベント発生] -> [ワーカー起動] -> [ハンドラ実行] -> [アイドル検出]
                                                          |
                                                          v
                                                   [30 秒後に終了]

Service Worker は 休眠できる。約 30 秒のアイドルでブラウザが終了させる。次のイベントで再び起きる。

これが意味すること:

  • グローバル変数に状態を持てない。 ワーカーが死ぬと消える。chrome.storage.sessionchrome.storage.local を使う。
  • タイマーは生き残らない。 setTimeout/setInterval の代わりに chrome.alarms を使う。
  • WebSocket を永続的に保てない。 切断される。必要なら Offscreen Document で保持する。
  • DOM がない。 ワーカーコンテキストでは DOM API が使えない。パースが必要なら Offscreen Document。

典型的な Service Worker パターン

// background.ts
chrome.runtime.onInstalled.addListener(() => {
  console.log('Extension installed')
  chrome.storage.local.set({ installedAt: Date.now() })
})

chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => alert('Hello from extension!'),
  })
})

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'PING') {
    sendResponse({ type: 'PONG', timestamp: Date.now() })
  }
  return true // 非同期応答のため
})

// 定期作業は alarms で
chrome.alarms.create('hourly-sync', { periodInMinutes: 60 })
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'hourly-sync') {
    syncData()
  }
})

async function syncData() {
  const res = await fetch('https://api.example.com/sync')
  const data = await res.json()
  await chrome.storage.local.set({ lastSync: data })
}

keepAlive アンチパターン

初期の頃、ワーカー終了に戸惑った開発者は setInterval(() => fetch('/ping'), 20000) でワーカーを強制的に生かそうとした。2026 年には明らかなアンチパターンだ。Chrome はそうしたワーカーも結局終了させるし、ポリシー上も推奨されない。正攻法は 状態を storage に保存し、イベントごとに起きて処理することだ。


第 4 章 · declarativeNetRequest — 広告ブロックへの影響

MV3 で最も議論を呼んだ変更。blocking webRequest を declarativeNetRequest(DNR)に置き換え。

なぜ変えたのか

Chrome チームの公式見解: 性能とプライバシー。blocking webRequest はすべてのネットワークリクエストを拡張機能ワーカーに同期評価のためルーティングしていた。それが遅く、拡張機能が全トラフィックを覗ける権限が広すぎた。

反対派の見解: その権力を拡張機能から奪い 宣言的ルールに閉じ込めたということは、複雑な広告ブロックロジック(特にコスメティックフィルタや動的ブロック)が弱体化したという意味だ。

DNR の基本

{
  "manifest_version": 3,
  "name": "Simple Ad Blocker",
  "version": "1.0.0",
  "permissions": ["declarativeNetRequest"],
  "host_permissions": ["<all_urls>"],
  "declarative_net_request": {
    "rule_resources": [
      {
        "id": "ruleset_1",
        "enabled": true,
        "path": "rules.json"
      }
    ]
  }
}
[
  {
    "id": 1,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "||doubleclick.net^",
      "resourceTypes": ["script", "image", "sub_frame"]
    }
  },
  {
    "id": 2,
    "priority": 1,
    "action": { "type": "redirect", "redirect": { "url": "https://example.com/blocked" } },
    "condition": {
      "urlFilter": "||tracker.com^",
      "resourceTypes": ["main_frame"]
    }
  }
]

ルールは 宣言的。拡張機能はリクエストごとに判断せず、ブラウザがルールセットを参照して決める。

制約

  • 静的ルールの上限: 単一拡張機能で 30,000 個。有効化されたすべての拡張機能の合計で 330,000 個。uBlock Origin が使っていた 100 万件超の EasyList ルールは全部入らない。
  • 動的ルール: 5,000 個。ユーザーが追加できるルール。
  • セッションルール: 5,000 個。ブラウザ終了時に消える。
  • 正規表現ルール: マッチ時間制限が厳格。

uBlock Origin の作者 Raymond Hill は、Chrome では uBlock Origin Lite という MV3 互換版を別途運用しているが、「MV2 版 uBO ほどの性能は出せない」と明記している。Firefox では今もフル機能の uBO が動く(Mozilla が blocking webRequest を維持する選択をした)。


第 5 章 · Side Panel API(Chrome 114+)

2023 年 6 月の Chrome 114 で導入された Side Panel API は、拡張機能の UX を根本的に変えた。ポップアップがクリックで閉じる揮発性 UI だとすると、Side Panel は ブラウザ横に永続する UI だ。

基本

{
  "manifest_version": 3,
  "name": "Side Panel Demo",
  "version": "1.0.0",
  "permissions": ["sidePanel"],
  "side_panel": {
    "default_path": "sidepanel.html"
  },
  "action": {
    "default_title": "Open Side Panel"
  }
}
// background.ts
chrome.runtime.onInstalled.addListener(() => {
  chrome.sidePanel
    .setPanelBehavior({ openPanelOnActionClick: true })
    .catch((error) => console.error(error))
})

// 特定のタブだけサイドパネルを有効化
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
  if (!tab.url) return
  const url = new URL(tab.url)
  if (url.origin === 'https://www.example.com') {
    await chrome.sidePanel.setOptions({
      tabId,
      path: 'sidepanel-example.html',
      enabled: true,
    })
  } else {
    await chrome.sidePanel.setOptions({
      tabId,
      enabled: false,
    })
  }
})

Side Panel の強み

  • 永続性: タブを切り替えても同じパネルが残る(タブ別にすることも可能)。
  • 広さ: ポップアップは 800x600 程度が限界だが、サイドパネルはもっと広い。
  • 対話性: ユーザーが作業を続ける間、パネルが見え続ける。
  • AI アシスタントとの相性: チャット UI がサイドパネルに自然に収まる。

Edge もサイドパネルをサポート(Edge の Copilot がまさにこの構造)。Firefox は MV2 時代から sidebarAction という似た API を持っていて、MV3 でも維持。Safari は一部バージョンでサポート。


第 6 章 · Offscreen Documents + Scripting API

Offscreen Documents — DOM が必要なとき

Service Worker には DOM がない。だが拡張機能はときどき本当に DOM が必要だ — HTML パース、クリップボードアクセス、<audio> 再生、DOMParser、WebRTC など。

解決策: Offscreen Document。ユーザーに見えない HTML ページをバックグラウンドで立ち上げられる。

{
  "permissions": ["offscreen"]
}
// background.ts
async function ensureOffscreenDocument() {
  const existing = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT'],
    documentUrls: [chrome.runtime.getURL('offscreen.html')],
  })
  if (existing.length > 0) return

  await chrome.offscreen.createDocument({
    url: 'offscreen.html',
    reasons: ['DOM_PARSER'],
    justification: 'Parse HTML for content extraction',
  })
}

chrome.action.onClicked.addListener(async () => {
  await ensureOffscreenDocument()
  const response = await chrome.runtime.sendMessage({
    target: 'offscreen',
    type: 'PARSE_HTML',
    data: '<html>...</html>',
  })
  console.log('parsed:', response)
})
<!-- offscreen.html -->
<!doctype html>
<html>
  <body>
    <script src="offscreen.js"></script>
  </body>
</html>
// offscreen.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.target !== 'offscreen') return
  if (message.type === 'PARSE_HTML') {
    const parser = new DOMParser()
    const doc = parser.parseFromString(message.data, 'text/html')
    const title = doc.querySelector('title')?.textContent ?? ''
    sendResponse({ title })
  }
})

reasons は固定 enum から選ぶ: AUDIO_PLAYBACKCLIPBOARDDOM_PARSERDOM_SCRAPINGIFRAME_SCRIPTINGLOCAL_STORAGETESTINGUSER_MEDIAWEB_RTCBLOBSWORKERS

Scripting API — コンテンツスクリプトの未来

MV2 の chrome.tabs.executeScript は MV3 の chrome.scripting.executeScript に置き換わった。

// 関数の注入
await chrome.scripting.executeScript({
  target: { tabId: 123 },
  func: (color) => {
    document.body.style.backgroundColor = color
  },
  args: ['lightblue'],
})

// ファイルの注入
await chrome.scripting.executeScript({
  target: { tabId: 123, allFrames: true },
  files: ['content.js'],
})

// CSS の挿入
await chrome.scripting.insertCSS({
  target: { tabId: 123 },
  css: 'body { filter: invert(1); }',
})

// コンテンツスクリプトの動的登録
await chrome.scripting.registerContentScripts([
  {
    id: 'dynamic-script',
    matches: ['https://*.example.com/*'],
    js: ['dynamic.js'],
    runAt: 'document_idle',
  },
])

scripting 権限 + ホスト権限(または activeTab)の組み合わせが必要。


第 7 章 · Plasmo — React ベースのフレームワーク

伝統的な拡張機能開発は、manifest を手書きし、webpack/rollup を設定し、ホットリロードを自分で組み上げる… 退屈だった。Plasmo はそれら全部を Next.js のように ほどく。

30 秒セットアップ

pnpm create plasmo my-extension
cd my-extension
pnpm dev
my-extension/
├── popup.tsx          # 自動でポップアップになる
├── options.tsx        # 自動でオプションページ
├── background.ts      # 自動でサービスワーカー
├── contents/
│   └── plasmo.ts      # コンテンツスクリプト
├── sidepanel.tsx      # サイドパネル
└── package.json       # マニフェストの一部

manifest.json を直接書く必要はない。ファイル名とディレクトリの規約から決まる。メタデータは package.json に書く。

{
  "name": "my-extension",
  "displayName": "My Extension",
  "version": "1.0.0",
  "description": "...",
  "manifest": {
    "permissions": ["storage", "tabs"],
    "host_permissions": ["https://*.example.com/*"]
  }
}

Plasmo の強み

  • React 一等市民: popup.tsx がそのまま React コンポーネント。
  • HMR: 拡張機能のコードを編集すると自動リロード。
  • Content Script UI: contents/ に React コンポーネントを置けばページに注入される UI を React で書ける。
  • Storage Hook: @plasmohq/storageuseStorage フック。
  • Messaging: 型安全なメッセージング。
  • クロスブラウザ: Chrome / Firefox / Edge ビルドを分離。

Content Script UI の例

// contents/inline.tsx
import type { PlasmoCSConfig } from 'plasmo'
import { useState } from 'react'

export const config: PlasmoCSConfig = {
  matches: ['https://*.example.com/*'],
}

export default function ContentUI() {
  const [count, setCount] = useState(0)
  return (
    <div style={{ position: 'fixed', top: 10, right: 10, background: 'white', padding: 12 }}>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
    </div>
  )
}

これが自動でページに注入されて動く。Shadow DOM 隔離もオプションで可能。

Plasmo の弱み

  • 抽象が厚いので、manifest を細かく制御したいときに不自由なときがある。
  • ビルド成果物が重くなる傾向(React ランタイム同梱)。
  • かつて活発だった BrowserStack(親会社)のサポートが減ったとの観測。とはいえ OSS コミュニティは健在。

第 8 章 · WXT — Vite ベースの新鋭

Plasmo が「React/Next.js」アナロジーなら、WXT は「Vite/Nuxt」のそれだ。より軽く、より速く、より自由。2023 年の登場以降、2025-2026 年に急成長。

基本構造

pnpm dlx wxt@latest init my-extension
cd my-extension
pnpm dev
my-extension/
├── entrypoints/
│   ├── background.ts
│   ├── content.ts
│   ├── popup/
│   │   └── index.html
│   └── options/
├── wxt.config.ts
└── package.json
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
  manifest: {
    permissions: ['storage', 'tabs'],
    host_permissions: ['https://*.example.com/*'],
  },
  modules: ['@wxt-dev/module-react'],
})
// entrypoints/background.ts
export default defineBackground(() => {
  console.log('Hello from background!')
  browser.runtime.onMessage.addListener((msg) => {
    return Promise.resolve({ pong: true })
  })
})

WXT の強み

  • Vite ベース: ビルドが速く、ESM フレンドリー。
  • フレームワーク非依存: React モジュール、Vue モジュール、Svelte もある。
  • 自動 import: Nuxt スタイルの自動 imports — browserdefineBackgrounddefineContentScript がグローバルとして自動で入ってくる。
  • クロスブラウザ: 一度書いて Chrome/Firefox/Safari ビルドを分離。
  • Web-ext 統合: 開発モードでブラウザを自動起動しホットリロード。

WXT vs Plasmo 一行比較

項目PlasmoWXT
基盤Parcel/Next.js 風Vite/Nuxt 風
フレームワークReact 中心(Vue/Svelte も可)モジュールで対等
Manifest 制御規約優先直接記述可能
学習曲線低(規約に従えばよい)中(設定は明示的)
コミュニティ2022-2024 活発 → 停滞2024-2026 急成長
推奨シナリオReact 中心の素早いプロトタイプ長期運用・多フレームワーク・精密制御

2026 年時点で新規スタートなら WXT が優勢というのが多数意見。Plasmo もまだ良いが、勢いは WXT に移った。


第 9 章 · vite-plugin-web-extension — ミニマル選択肢

フレームワークの重さが負担なら、vite-plugin-web-extension が答えかもしれない。Vite プラグイン 1 つで拡張機能のビルドを解決する。

pnpm add -D vite vite-plugin-web-extension
// vite.config.ts
import { defineConfig } from 'vite'
import webExtension from 'vite-plugin-web-extension'
import path from 'path'

export default defineConfig({
  plugins: [
    webExtension({
      manifest: path.resolve('manifest.json'),
      additionalInputs: {
        html: ['sidepanel.html'],
      },
    }),
  ],
})

manifest.json を直接書き、Vite がエントリを自動発見してビルドする。規約による自動化はないが、Vite のすべてをそのまま使える。

選択基準

  • 「フレームワーク依存なしで軽く」→ vite-plugin-web-extension
  • 「Vite エコシステム + ある程度の自動化」→ WXT
  • 「React 中心で素早く」→ Plasmo
  • 「フレームワーク不使用の素のまま」→ webpack/Rollup 直接(現在は非推奨)

第 10 章 · Firefox MV3 — 一部 MV2 を維持

Mozilla は Chrome の MV3 推進に微妙な反対の立場だった。結果として Firefox MV3 は Chrome MV3 に似ているが、部分的に異なる。

Firefox MV3 の差別点

  • Background: Service Worker ではなく イベントページモデル。休眠可能だが DOM を持てる。
  • blocking webRequest: 維持。Firefox で uBlock Origin がフル機能で動く理由。
  • ホスト権限: Chrome 同様、ランタイムオプトイン可能。
  • API 名前空間: browser.*(Promise ベース)が標準。chrome.* も互換のため動く。

同じコードを両方で動かす

// 共通ヘルパー
const api = (typeof browser !== 'undefined' ? browser : chrome) as typeof browser

async function getCookie(url: string, name: string) {
  return api.cookies.get({ url, name })
}

webextension-polyfill ライブラリを使えば chrome.* も Promise を返すようにできる。

import browser from 'webextension-polyfill'

await browser.storage.local.set({ key: 'value' })

Plasmo と WXT はこの polyfill または同等の抽象を内蔵している。

manifest の差分

Firefox は manifest.jsonbrowser_specific_settings を要求することがある。

{
  "manifest_version": 3,
  "name": "Cross-Browser Ext",
  "version": "1.0.0",
  "background": {
    "scripts": ["background.js"],
    "type": "module"
  },
  "browser_specific_settings": {
    "gecko": {
      "id": "myext@example.com",
      "strict_min_version": "115.0"
    }
  }
}

WXT はこの分岐を自動で処理してくれる。


第 11 章 · Safari Web Extensions — Mac/iOS

Apple は 2020 年の Safari 14 で Web Extensions を受け入れた。2021 年の iOS 15 から iPhone/iPad でも 拡張機能が可能になった。これは小さな出来事ではない — モバイル拡張機能エコシステムの事実上初の例だ。

Safari Web Extension の特徴

  • Xcode プロジェクトでラップ: Web 拡張機能コードを Xcode アプリコンテナに入れてビルド。App Store 経由で配布。
  • browser.* API を使用: Mozilla 互換の名前空間。
  • 一部 API は未サポート: chrome.identitychrome.sidePanelchrome.declarativeNetRequest の一部、chrome.offscreen などが未対応/制限。
  • iOS 制約: モバイルでは権限モデルがより厳格。ユーザーの明示的な有効化が必要。

変換ツール — safari-web-extension-converter

既存の Chrome/Firefox 拡張機能を Safari に移植するとき:

xcrun safari-web-extension-converter /path/to/extension

このコマンドが Xcode プロジェクトの足場を作る。manifest を読み、互換のない API を警告で知らせる。

配布

  • macOS: Safari Extensions Gallery は 2022 年に閉鎖。今はすべての Safari 拡張機能が App Store 経由で配布。
  • iOS: 同じく App Store。
  • Apple Developer Program メンバーシップが必要(年 $99)。

この参入障壁が、Safari 拡張機能のエコシステムが Chrome ほど豊かでない理由。だが iOS で拡張機能が可能な唯一の道なので、ますます重要になる。


第 12 章 · Edge Add-ons — Microsoft

Edge は Chromium ベースなので Chrome 拡張機能がほぼそのまま動く。だが Microsoft は Edge Add-ons という独自ストアを運営している。

Edge に拡張機能を載せる

  1. Microsoft Partner Center に開発者アカウント登録(無料)。
  2. 同じ .zip パッケージをアップロード。
  3. レビュー。

Chrome 拡張機能との互換性

  • ほぼ 100%: Chrome MV3 拡張機能は Edge でほぼそのまま動く。
  • Edge 特化 API: 一部の chrome.* の代わりに edge.* として公開される API があるが、大部分は同じ。
  • サイドバー: Edge は Sidebar API を積極活用(Copilot 等)。

Edge でだけ可能なこと

  • 強制インストールポリシー: エンタープライズ環境で IT 管理者が強制配布可能。
  • Bing Chat / Copilot 統合: マイクロソフトのサービスとの統合 API。
  • Edge Workspaces: ワークスペースに紐づく拡張機能。

Edge のシェアは世界で 5-7% 程度と小さく見えるが、エンタープライズ環境では Chrome に劣らず重要。B2B 拡張機能なら Edge ストア登録は必須。


第 13 章 · AI 連携拡張機能 — Side Panel + LLM

2026 年の拡張機能開発で最も興味深い流れは AI 連携だ。ChatGPT ブラウザ拡張機能、Perplexity、Glasp、Compose AI、Wisedocs といったツールがみな同じパターンを使う。

典型的な AI 拡張機能アーキテクチャ

[Web Page]
   |  (ユーザーアクション: テキスト選択、ページ要約リクエスト)
   v
[Content Script] -- ページコンテキスト抽出 -->
   |
   v
[Service Worker] -- LLM API 呼び出し(Claude / OpenAI / 自前) -->
   |
   v
[Side Panel] -- 結果をストリーミング表示 -->
   |
   v
[ユーザー]

Side Panel + ストリーミング UI

// sidepanel/App.tsx (WXT + React)
import { useState } from 'react'

export default function SidePanelApp() {
  const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; content: string }[]>([])
  const [input, setInput] = useState('')

  async function send() {
    const userMsg = { role: 'user' as const, content: input }
    setMessages((m) => [...m, userMsg])
    setInput('')

    const response = await chrome.runtime.sendMessage({
      type: 'ASK_LLM',
      messages: [...messages, userMsg],
    })

    setMessages((m) => [...m, { role: 'assistant', content: response.text }])
  }

  return (
    <div className="flex flex-col h-screen p-4">
      <div className="flex-1 overflow-auto">
        {messages.map((m, i) => (
          <div key={i} className={m.role === 'user' ? 'text-blue-600' : 'text-gray-800'}>
            {m.content}
          </div>
        ))}
      </div>
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && send()}
          className="flex-1 border p-2"
        />
        <button onClick={send} className="bg-blue-500 text-white px-4">
          Send
        </button>
      </div>
    </div>
  )
}
// background.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'ASK_LLM') {
    callLLM(msg.messages).then((text) => sendResponse({ text }))
    return true // async
  }
})

async function callLLM(messages: any[]) {
  const apiKey = (await chrome.storage.local.get('apiKey')).apiKey
  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      model: 'claude-opus-4-7',
      max_tokens: 1024,
      messages,
    }),
  })
  const data = await res.json()
  return data.content[0].text
}

ページコンテキストの活用

// content.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'GET_PAGE_TEXT') {
    sendResponse({
      title: document.title,
      url: location.href,
      text: document.body.innerText.slice(0, 50000),
    })
  }
})
// sidepanel: 現在のタブのテキストを取得して要約リクエスト
async function summarizeCurrentPage() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  if (!tab.id) return
  const pageData = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TEXT' })
  // ... LLM 呼び出し
}

Chrome Built-in AI(Gemini Nano)

2024 年から Chrome は オンデバイス Gemini Nano を拡張機能で使えるよう提供を開始した(Origin Trial → GA)。chrome.ai という名前空間が段階的に解放されており、2026 年 5 月時点で一部 API(prompt、summarize、translate)が安定チャネル。

// 可用性確認
const availability = await ai.languageModel.availability()
if (availability === 'available') {
  const session = await ai.languageModel.create()
  const response = await session.prompt('Summarize: ...')
}

長所: ローカル実行、無料、プライバシー。短所: モデルが小さいので精緻な作業には限界。

ビジネスモデル

AI 拡張機能のよくあるビジネスモデル 4 つ。

  1. BYOK(Bring Your Own Key): ユーザーが自分の API キーを入れる。開発者にトラフィックコストがかからない。
  2. 無料枠 + 有料: 一定回数まで無料、以降サブスク。
  3. 純粋 SaaS: 拡張機能はクライアント、認証/課金は自前バックエンド。
  4. オンデバイス: Chrome Built-in AI または自前の量子化モデルでコスト 0。

第 14 章 · 韓国 / 日本 — Kakao、Naver、pixiv

韓国

  • Kakao 拡張機能: KakaoTalk シェア、Daum 検索、Kakao Bank 認証など Kakao エコシステムと連携するユーティリティ拡張機能が多い。Kakao は公式に拡張機能 SDK を出してはいないが、Kakao API と OAuth を拡張機能から使う事例が豊富。
  • Naver 拡張機能: Naver 検索結果の強化、Naver Cloud、Naver 辞書、Naver Shopping 価格比較(Signal、Danawa のような比較ショッピング拡張機能)が代表的。
  • Toss / Karrot: 決済・中古取引領域で社内向け拡張機能を運用する事例が多い。
  • 翻訳器: Papago、Naver 翻訳の拡張機能。モバイル優勢の韓国で、デスクトップ翻訳は今も有効な市場。
  • 広告ブロック: AdBlock Plus の EasyList Korea が活発。

日本

  • pixiv 拡張機能: pixiv 作品ダウンローダ、タグ強化、イラストビューア補助といった非公式 OSS が多数。ただし pixiv ToS と衝突する可能性が常にあるので慎重に。
  • Niconico 拡張機能: 動画ダウンロード補助、コメント強化、ビューア改善など。
  • Mercari / Rakuma: 中古マーケットプレイスの価格追跡拡張機能。
  • Rakuten 拡張機能: 楽天ポイントの自動計算、クーポン自動適用。
  • 日本語学習: Rikaikun、10ten Japanese Reader、Yomichan(現在は Yomitan としてフォーク) — 日本語学習者には事実上必携。
  • AdGuard: 日本語 EasyList Japan のメンテナンスが活発。

Kakao/Naver OAuth in 拡張機能

// chrome.identity.launchWebAuthFlow による OAuth
async function loginWithKakao() {
  const clientId = 'YOUR_KAKAO_REST_API_KEY'
  const redirectUri = chrome.identity.getRedirectURL()
  const authUrl =
    `https://kauth.kakao.com/oauth/authorize?` +
    `client_id=${clientId}&` +
    `redirect_uri=${encodeURIComponent(redirectUri)}&` +
    `response_type=code`

  const responseUrl = await chrome.identity.launchWebAuthFlow({
    url: authUrl,
    interactive: true,
  })

  const code = new URL(responseUrl!).searchParams.get('code')
  // code をバックエンドに送ってトークン交換
  return code
}

chrome.identity.getRedirectURL() が返す URL を Kakao 開発者コンソールの Redirect URI に登録する必要がある。形式は https://<extension-id>.chromiumapp.org/

韓国・日本の拡張機能開発のリアル

  • 決済モジュール(Kakao Pay、Naver Pay、PayPay)を拡張機能から直接呼ぶのはほぼ不可能。バックエンド経由が必須。
  • インアプリブラウザ(KakaoTalk、Line の内部ブラウザ)は拡張機能をサポートしない。モバイルで拡張機能を狙うなら iOS の Safari Web Extension がほぼ唯一の選択肢。
  • 日本は保守的な市場なので、拡張機能の採用率は韓国・米国より低い傾向。代わりに一度入れると長く使う。

第 15 章 · 誰が何を選ぶべきか — 個人 / チーム / クロスブラウザ / AI 連携

個人開発者 + 素早いプロトタイプ

  • 推奨スタック: Plasmo または WXT。
  • 理由: HMR、規約、ボイラープレート最小化。1 週間以内に使える拡張機能を出せる。
  • ターゲットブラウザ: Chrome 優先、うまくいけば Firefox/Edge へ拡張。
  • 注意点: Web Store レビュー時間(平均 1-7 日)を見込む。

小規模チーム + 長期プロダクト

  • 推奨スタック: WXT。
  • 理由: Vite ベースでビルドが速く、TS フレンドリー、マルチブラウザサポートが自然。
  • 推奨: Storybook や Chromatic で UI 回帰テスト。E2E は Playwright の拡張機能モード(BrowserContext に拡張機能をロード)。

クロスブラウザ優先

  • 推奨スタック: WXT + webextension-polyfill
  • ビルドマトリクス: Chrome MV3、Firefox MV3、Edge MV3、Safari(別 Xcode プロジェクト)。
  • 注意点: declarativeNetRequest 使用時は Firefox では webRequest にフォールバック分岐が必要なことがある。

AI 連携拡張機能

  • UI: Side Panel 優先。ポップアップは短いアクション専用。
  • LLM: BYOK モデルから始めて、ユーザー数が増えたら自前バックエンドを検討。
  • オンデバイス: Chrome Built-in AI(Gemini Nano)も選択肢。無料という点が強い。
  • コンテキスト抽出: content script でページテキスト + メタデータを取得 → Service Worker から LLM 呼び出し → Side Panel でストリーミング表示。

広告ブロック / コンテンツフィルタリング

  • MV3 Chrome: DNR の上限内で可能。フル機能は難しい。
  • Firefox: blocking webRequest でフル機能可能。uBlock Origin が Firefox でまだ生きている理由。
  • 推奨: Firefox をメインターゲット、Chrome は Lite 版で分岐。

エンタープライズ / B2B

  • Chrome: エンタープライズポリシーで強制配布可能(force_install)。
  • Edge: 同じ方式。Microsoft 365 環境で自然。
  • 推奨: 会社ドメインの SAML SSO 連携が可能なバックエンドを構築。拡張機能はクライアント役。

第 16 章 · 拡張機能セキュリティ — 何に気をつけるか

ホスト権限は最小に

{
  "permissions": ["activeTab"],
  "optional_host_permissions": ["https://*.example.com/*"]
}

<all_urls> はレビューで strict に見られる。activeTab を優先し、広範な権限が本当に必要なら optional_host_permissions でランタイム要求。

CSP を厳格に

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  }
}

unsafe-evalunsafe-inline は MV3 で禁止。外部 CDN スクリプトも禁止(すべての JS をパッケージに同梱しなければならない)。

externally_connectable

Web ページが拡張機能にメッセージを送れるようにするには:

{
  "externally_connectable": {
    "matches": ["https://yoursite.com/*"]
  }
}

これを通じて自社サイトが拡張機能と通信するパターンが可能だが、ドメインを狭く取らないとセキュリティホールになる。

XSS 防御

  • content script でページ DOM に何かを注入する際は textContent を使い、innerHTML は避ける。
  • React を使えば基本的に安全だが、dangerouslySetInnerHTML は慎重に。
  • Trusted Types の適用を検討。

個人情報

  • API キーは chrome.storage.local に保存しつつ、可能なら BYOK またはバックエンド経由のトークン交換。
  • ページコンテンツを外部に送るときはユーザーに明示的に伝え、オプトインさせる。
  • Chrome Web Store のデータ使用開示ポリシーを必ず守る — 違反すると拡張機能がブロックされる。

第 17 章 · Chrome Web Store ポリシー — 通すコツ

よくある却下理由

  1. 権限過多: <all_urls> または広範なホスト権限の正当性不足。
  2. 単一目的違反: 1 拡張機能は明確な単一目的。「複数ユーティリティを束ねた」は却下事由。
  3. コンテンツポリシー: 広告挿入、検索結果操作、アフィリエイト横取りは strict に禁止。
  4. ユーザーデータポリシー: データ収集時に privacy policy URL 必須、明示的な開示。
  5. ソースコード難読化: 意図的な難読化は禁止。minify は OK だが readable でなければならない。
  6. 決済回避: Chrome Web Store Payments を使わず外部決済で迂回するのは制限。

レビュー時間

  • 新規登録: 平均 1-7 日、長いと 2-3 週間。
  • 更新: 通常 1-2 日。
  • センシティブな権限(<all_urls>tabsdownloads)があると延びる

ベータチャネル活用

unlisted または private 配布でベータテスターに先に配布した後、public に切り替え。レビューはもう一度通る。

ユーザーデータ開示(Privacy Practices)

Chrome Web Store は次を明示的に開示するよう要求:

  • どんなユーザーデータを扱うか
  • なぜ扱うか
  • どう保護するか
  • どこに送信するか

虚偽の開示は即時のブロック事由。


第 18 章 · テストと CI/CD

ユニットテスト

// Vitest でヘルパー関数をテスト
import { describe, it, expect, vi } from 'vitest'
import { syncData } from './background'

global.chrome = {
  storage: {
    local: {
      set: vi.fn(),
      get: vi.fn().mockResolvedValue({}),
    },
  },
  alarms: {
    create: vi.fn(),
    onAlarm: { addListener: vi.fn() },
  },
} as any

describe('syncData', () => {
  it('saves data to storage', async () => {
    await syncData()
    expect(chrome.storage.local.set).toHaveBeenCalled()
  })
})

E2E — Playwright with Extension

import { test, expect, chromium } from '@playwright/test'
import path from 'path'

test('extension loads', async () => {
  const pathToExtension = path.resolve(__dirname, '../.output/chrome-mv3')
  const context = await chromium.launchPersistentContext('', {
    headless: false,
    args: [
      `--disable-extensions-except=${pathToExtension}`,
      `--load-extension=${pathToExtension}`,
    ],
  })

  const page = await context.newPage()
  await page.goto('https://example.com')

  // content script が注入されたか確認
  const injected = await page.evaluate(() => (window as any).__MY_EXT_LOADED__)
  expect(injected).toBe(true)

  await context.close()
})

CI/CD

# .github/workflows/release.yml
name: Build and Release
on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install
      - run: pnpm test
      - run: pnpm build --target chrome-mv3
      - run: pnpm build --target firefox-mv3
      - uses: actions/upload-artifact@v4
        with:
          name: extensions
          path: .output/

  publish-chrome:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - name: Upload to Chrome Web Store
        uses: PlasmoHQ/bpp@v3
        with:
          keys: $TEST_KEYS_PLACEHOLDER
          chrome-file: extensions/chrome-mv3.zip

PlasmoHQ/bpp(Browser Plugin Publisher)アクションが Chrome/Firefox/Edge への自動アップロードをサポート。


第 19 章 · よくある落とし穴

「Service Worker が死んで作業が途切れる」

  • 原因: 30 秒のアイドル後に終了。
  • 対処: 状態を chrome.storage に保存。長時間作業は chrome.alarmschrome.notifications で進捗表示。

「WebSocket が切れる」

  • 原因: Service Worker 終了。
  • 対処: Offscreen Document に WebSocket を置き、ワーカーとメッセージパッシング。

「content script からページの変数にアクセスできない」

  • 原因: Content Script は 隔離コンテキスト(isolated world) で実行。ページの window は見えない。
  • 対処: chrome.scripting.executeScriptworld: 'MAIN' オプション、または script タグ注入。

「Chrome では動くが Firefox では動かない」

  • 原因: API 差分(特に DNR 制限、ホスト権限モデル)。
  • 対処: WXT/Plasmo のクロスブラウザビルド、webextension-polyfill、分岐コード。

「レビューで却下された — 権限が過剰」

  • 原因: <all_urls> または広範なホスト権限。
  • 対処: activeTab 優先、本当に必要なドメインだけ明示。

「拡張機能が突然無効化された — ポリシー違反」

  • 原因: データ使用開示の欠落、単一目的違反など。
  • 対処: Chrome Web Store 開発者ダッシュボードで違反事由を確認、修正後に再提出。

第 20 章 · エピローグ — 拡張機能は死んだのか、生きているのか

2020 年代初頭には「PWA が拡張機能を置き換える」という言説があった。2026 年現在、その予測は半分しか当たっていない。PWA は拡張機能の一部の領域(インストール型 Web アプリ)を取ったが、ブラウザのすべてのページとインタラクションする能力は拡張機能だけが持つ。だから拡張機能は死なない。

むしろ MV3 の強制適用と AI の隆盛が 拡張機能のルネサンスをもたらした。ChatGPT、Claude、Perplexity の拡張機能は数百万ユーザーを集め、毎日新しい AI 補助拡張機能がリリースされる。Side Panel API はチャット UI のためのキャンバスになり、ページコンテキストを自由に LLM に送れることは拡張機能だけの強みだ。

2026 年の拡張機能開発の推奨を一行に圧縮すれば:

WXT か Plasmo で始めよ。Manifest V3、Service Worker、Side Panel API を身につけよ。Chrome を最初に、Firefox/Edge は自動ビルドで、Safari は市場があれば別プロジェクトで。AI 連携なら Side Panel + LLM API から。権限は最小に、レビューポリシーは正確に、ユーザーデータ開示は正直に。

これが 2026 年 5 月時点での正解。1 年後にはまた変わる — それが拡張機能の世界だ。


参考文献 / References