Chrome拡張はどう動くのか――Manifest V3の構成と処理の流れ
manifest.json、Content Script、Service Worker、popupという部品は、どうつながって動くのか。実装でつまずきやすい実行コンテキストの分離、Service Workerが常駐しないこと、メッセージング、権限の設計を、短いコード例とあわせて整理する。

Legacy Toolsの道具は、どれもChrome拡張として作っています。なぜWebサービスではなくChrome拡張にしたのかは以前書きました。今回はその続きとして、Chrome拡張が中でどう動いているのかを、実装の側から整理します。
Chrome拡張は「HTMLとJavaScriptで作れます」と紹介されることが多く、それ自体は正しいのですが、実際に作りはじめてつまずくのは、ファイルの書き方ではなく部品と部品の境界です。JavaScriptがどこで実行されるのか。Service Workerはいつ止まるのか。権限はどこまで及ぶのか。この記事は、その境界に絞って書きます。
ファイル構成から見る
小さな拡張の構成は、おおよそ次のようになります。
my-extension/
├─ manifest.json
├─ background.js
├─ content.js
├─ popup/
│ ├─ popup.html
│ ├─ popup.css
│ └─ popup.js
├─ options/
│ ├─ options.html
│ └─ options.js
└─ icons/
├─ icon16.png
├─ icon48.png
└─ icon128.png
各ファイルの役割を、ひとことで書くとこうです。
manifest.json Chromeに構成・権限・起動条件を伝える
content.js Webページ上でDOMを監視・操作する
background.js ブラウザ側のイベント処理を担当する
popup ツールバーから開く小さな画面
options 継続的な設定を変更する画面
役割が分かれているのは、整理のためだけではありません。それぞれのJavaScriptは別々の場所で実行され、できることも違います。
manifest.jsonがChromeに伝えていること
manifest.jsonは、拡張の構成・権限・起動条件をChromeに伝えるファイルです。短い実例を載せます。
{
"manifest_version": 3,
"name": "Example Extension",
"version": "1.0.0",
"permissions": ["storage"],
"host_permissions": ["https://example.com/*"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://example.com/*"],
"js": ["content.js"]
}
],
"action": {
"default_popup": "popup/popup.html"
}
}
大事なのは項目の暗記ではなく、項目どうしの関係です。
permissions:chrome.storageなどのChrome APIを使うための権限host_permissions:どのWebサイトへアクセスできるかcontent_scripts.matches:どのページでcontent.jsを実行するかbackground.service_worker:裏側でイベントを処理するファイルaction.default_popup:拡張アイコンを押したときに開く画面
permissionsとhost_permissionsが分かれている点は、あとで書く権限の話につながります。APIを使う権限と、サイトに触る権限は、別のものとして管理されています。
Content ScriptとWebページの境界
Chrome拡張を理解するうえでいちばん重要なのは、ファイル名ではなく、JavaScriptがどこで実行されるかです。
Webページ
├─ ページ自身のJavaScript
└─ Content Script
↓ メッセージ
Extension Service Worker
↓
chrome.storage / tabs / scripting など
Content ScriptはWebページのDOMを読み書きできます。ただし、ページ本体のJavaScriptとは分離された「isolated world」という環境で動きます。そのため、次のような誤解が起こります。
// Defined by the page's own JavaScript
window.applicationState = {
loggedIn: true
};
// Not necessarily visible from the content script
console.log(window.applicationState);
DOMは共有しているのに、JavaScriptのグローバル空間は基本的に共有していない。ページのボタンや入力欄には触れるのに、ページのスクリプトが持っている変数はそのまま見えるとは限らない。この非対称を知っているかどうかで、Content Scriptの設計は変わります。
Service Workerは常駐しない
Manifest V3のService Workerを、常駐しているプロセスのように考えると実装を誤ります。
let currentSettings = {};
このようにメモリ上だけへ状態を持っても、Service Workerが停止して再起動すれば消えます。Chromeの公開ドキュメントでは、Service Workerはおおむね30秒間何もしないと停止し、イベントを受け取ると起動し直すと説明されています(2026-07-19時点)。
Service Workerは必要なイベントが発生したときに起動し、処理が終われば停止する可能性がある。メモリ上の変数を、永続的な状態として扱ってはいけない。
残したい値は、chrome.storageへ保存します。
await chrome.storage.local.set({
enabled: true,
targetSites: ["example.com"]
});
const settings = await chrome.storage.local.get([
"enabled",
"targetSites"
]);
部品どうしのメッセージング
Content ScriptとService Workerは実行環境が別なので、互いの関数を直接呼ぶことはできません。データはメッセージで受け渡します。Content ScriptからService Workerへ処理を頼む例です。
// content.js
const response = await chrome.runtime.sendMessage({
type: "GET_SETTINGS"
});
// background.js
chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
if (message.type === "GET_SETTINGS") {
chrome.storage.local.get("enabled").then((settings) => {
sendResponse(settings);
});
return true;
}
}
);
リスナーの最後にあるreturn trueは、飾りではありません。非同期処理のあとでsendResponseを呼ぶ場合は、文字どおりのtrueを返してメッセージチャネルを開いたままにする必要があります。これを忘れると、応答が届いたり届かなかったりする、原因の分かりにくい不具合になります。
権限も設計の一部
host_permissionsの書き方には、実装の楽さと権限の広さのトレードオフがあります。
<all_urls>を最初から要求する。実装の負担は小さいが、すべてのサイトへのアクセスを求める強い権限表示が出て、利用者が不安を感じやすい- 対象サイトを限定する。権限を最小にできるが、対応サイトを増やすたびに拡張の更新が必要になる
optional_host_permissionsとして宣言し、利用者が必要とするサイトだけ後から許可してもらう。権限を最小に保てるが、許可を求めるUIと権限の管理が複雑になる
どれが常に正しいというものではなく、道具の性質によって決まります。ただ、権限はmanifest.jsonに書く設定項目であると同時に、インストール時に利用者へそのまま表示されるものでもあります。実装の都合だけでは決められません。
Legacy Toolsではどう分けているか
Safe Privacy GateやSafe Attachment Checkがしていることも、簡略化すればこの記事の流れに沿っています。
- Content Scriptが送信ボタンやファイル添付を検知する
chrome.storageから利用者の設定を読む- 入力内容や添付ファイルをブラウザの中で確認する
- 問題の候補があれば確認ダイアログを出す
- 続けるかやめるかは、利用者が決める
責務の分け方も、ここまでに書いた境界に沿わせています。
Content Script
- DOMイベントの監視
- 入力欄や添付操作の検知
- 確認ダイアログの表示
Service Worker
- インストール時の初期化
- 設定やブラウザイベントの管理
- 必要なChrome APIの呼び出し
chrome.storage
- 有効・無効の設定
- 対象サイト
- カスタムルール
小さな拡張でも、境界から考える
この記事で書いたのは、実行コンテキスト、Service Workerのライフサイクル、メッセージング、権限の4つです。小さな拡張でも、この4つの境界は避けて通れません。逆に言えば、ここさえ押さえてしまえば、残りの多くは普通のHTMLとJavaScriptです。
ビルドツールやフレームワーク、テストの話は、今回は書きませんでした。境界の話と混ぜると焦点がぼやけるためで、必要になったら別の記事として書きます。