0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張機能の content script・popup・Service Worker が止まるタイミング【Manifest V3】

0
Last updated at Posted at 2026-10-25

はじめに

Chrome 拡張機能(Manifest V3)は、役割の異なるいくつかの部品で構成されます。この記事では、そのうち content script・popup・Service Worker の3つを扱います。3つは動く場所が異なります。

それぞれがいつ始まり、いつ止まるのかを、検証用の拡張機能で確かめました。

検証環境:Chrome 155.0.8059.86(arm64)、macOS 14.7

この記事で扱う3つの部品

image.png

部品 役割 manifest.json のキー
content script Web ページの中で動き、ページの DOM を読み書きするスクリプト content_scripts
popup ツールバーのアイコンをクリックすると開く画面 action.default_popup
Service Worker イベントを受け取って処理する、画面を持たないスクリプト background.service_worker

検証用の拡張機能を用意する

3つの部品が動いたタイミングをそれぞれログに記録し、popup に一覧で表示する拡張機能を作ります。

3つの部品のログを1か所で確認する

3つの部品は、console.log の出力先がそれぞれ異なります。

  • content script:そのページの DevTools
  • popup:popup を右クリックして「検証」で開く DevTools
  • Service Worker:chrome://extensions の「ビューを検証」から開く DevTools

Service Worker の DevTools を開いている間は、Service Worker が停止しません。そこで、DevTools を開かずにログを確認できるよう、ログを chrome.storage.local に保存して popup に一覧で表示します。content script も chrome.storage を直接使えるため、3つの部品のログを1か所で確認できます。

ファイル構成

lifecycle-demo/
├── manifest.json
├── log.js
├── content.js
├── popup.html
├── popup.js
└── service-worker.js

log.js は3つの部品で共通して使うログの関数、それ以外は部品ごとのファイルです。

manifest.json

manifest.json
manifest.json
{
  "manifest_version": 3,
  "name": "Lifecycle Demo",
  "version": "1.0",
  "permissions": ["storage"],
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/*"],
      "js": ["log.js", "content.js"]
    }
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

content_scripts・action・background で3つの部品を登録し、permissions でログの保存に使う storage 権限を宣言しています。content script は、matches で指定した https://developer.chrome.com/ のページにだけ挿入します。

log.js

3つの部品で共通して使う log 関数を、log.js に置きます。第1引数には、どの部品の記録かを表すラベル(CS:content script、popup、SW:Service Worker)を渡します。

log.js
let seq = 0;
 
function log(label, text) {
  const time = new Date().toLocaleTimeString();
  const order = String(seq++).padStart(5, '0');
  const key = `log:${Date.now()}:${order}:${crypto.randomUUID()}`;
  chrome.storage.local.set({ [key]: `${time} [${label}] ${text}` });
}

複数の部品が同時に書き込んでもログが失われないように、ログを1件ずつ別のキーで保存する関数です。キーは「記録した時刻(ミリ秒)・スクリプト内での連番・ランダムな ID」をつないだ文字列です。連番で同じミリ秒の記録の順番を保ち、ランダムな ID で部品どうしのキーの重なりを防いでいます。表示するときはキーの順に並べます。

log.js は、部品ごとに以下の方法で読み込みます。

  • content script:manifest.json の js で、content.js より先に並べる
  • popup:popup.html で、popup.js より先に <script> タグで読み込む
  • Service Worker:service-worker.js の先頭で importScripts('log.js') を呼ぶ

content.js

content.js
let count = 0;
count++;
log('CS', `ページに挿入された(count: ${count})`);

ページに挿入されるたびに、グローバル変数 count を1増やして記録するスクリプトです。

popup.html

popup.html
popup.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body style="width: 320px">
    <button id="send">Service Worker にメッセージを送る</button>
    <button id="timer">5秒後に記録する</button>
    <button id="clear">ログを消す</button>
    <pre id="logs"></pre>
    <script src="log.js"></script>
    <script src="popup.js"></script>
  </body>
</html>
実験で使う3つのボタンと、ログを表示する領域を置いた popup の画面です。

popup.js

popup.js
popup.js
async function render() {
  const items = await chrome.storage.local.get(null);
  const lines = Object.keys(items)
    .filter((key) => key.startsWith('log:'))
    .sort()
    .map((key) => items[key]);
  document.getElementById('logs').textContent = lines.join('\n');
}
 
log('popup', '開いた');
render();
chrome.storage.onChanged.addListener(render);
 
document.getElementById('send').addEventListener('click', () => {
  chrome.runtime.sendMessage({ type: 'count' });
});
 
document.getElementById('timer').addEventListener('click', () => {
  log('popup', '5秒タイマーを開始した');
  setTimeout(() => log('popup', '5秒たった'), 5000);
});
 
document.getElementById('clear').addEventListener('click', () => {
  chrome.storage.local.clear();
});

popup を開いたときに「開いた」と記録し、保存されているログを時刻順に表示します。chrome.storage.onChanged で保存内容の変化を受け取り、表示を更新します。ボタンを押すと、Service Worker へのメッセージ送信、5秒後に記録するタイマーの開始、ログの削除をそれぞれ行います。

service-worker.js

service-worker.js
importScripts('log.js');
 
let count = 0;
 
log('SW', '起動した');
 
chrome.runtime.onMessage.addListener((message) => {
  if (message.type === 'count') {
    count++;
    log('SW', `メッセージを受信した(count: ${count})`);
  }
});

先頭の importScripts('log.js') で log 関数を読み込みます。Service Worker が起動したときに「起動した」と記録し、メッセージを受け取るたびにグローバル変数 count を1増やして記録します。イベントリスナーをトップレベルで登録している理由は、「寿命を前提にした書き方」で説明します。

拡張機能を読み込む

chrome://extensions でデベロッパーモードを有効にし、「パッケージ化されていない拡張機能を読み込む」から lifecycle-demo フォルダを読み込みます。各実験の前には、popup の「ログを消す」でログを空にします。

content script はページを読み込むたびに作り直される

ページを再読み込みしてログを確認する

  • STEP 1:popup の「ログを消す」を押す
  • STEP 2:新しいタブで https://developer.chrome.com/ を開く
  • STEP 3:https://developer.chrome.com/ のタブで、ブラウザの再読み込みボタンを2回押す
  • STEP 4:popup を開いてログを確認する

実行結果は以下のとおりです。

14:10:01 [CS] ページに挿入された(count: 1)
14:10:05 [CS] ページに挿入された(count: 1)
14:10:08 [CS] ページに挿入された(count: 1)
14:10:12 [popup] 開いた

最初の読み込みと2回の再読み込みで、「ページに挿入された」が3回記録されています。count は3回とも 1 で、再読み込みのたびにグローバル変数が初期化されています。

content script が挿入されるタイミング

content script は、manifest.json の matches に一致するページが読み込まれるたびに挿入されます。挿入のタイミングは run_at で指定でき、既定値の document_idle では DOM の構築が終わった後に実行されます。

ページを再読み込みすると、新しく読み込まれたページに content script が改めて挿入されます。そのため、グローバル変数の値は前のページから引き継がれません。

popup を閉じると実行中のタイマーも止まる

タイマーの途中で popup を閉じる

  • STEP 1:popup を開き、「ログを消す」を押す
  • STEP 2:「5秒後に記録する」を押して、5秒たつ前に popup の外をクリックして閉じる
  • STEP 3:10秒ほど待ってから popup を開き直す
  • STEP 4:「5秒後に記録する」を押して、popup を開いたまま5秒待つ

実行結果は以下のとおりです。

14:15:01 [popup] 5秒タイマーを開始した
14:15:15 [popup] 開いた
14:15:17 [popup] 5秒タイマーを開始した
14:15:22 [popup] 5秒たった

STEP 2 では popup を閉じたため、「5秒たった」が記録されていません。popup を開いたままにした STEP 4 だけ、5秒後に記録されています。

popup が閉じるタイミング

popup は、アイコンのクリックなどで開き、popup の外にフォーカスが移ると自動で閉じます。公式ドキュメントでは、外をクリックした後も popup を開いたままにする方法はないと説明されています。

popup が閉じると、popup の JavaScript は実行中のタイマーも含めて消えます。開き直すと popup.js が最初から実行されるため、「開いた」がもう一度記録されます。

Service Worker は何もしないと30秒で停止する

1分おいてからメッセージを送る

  • STEP 1:Service Worker の DevTools を閉じておく
  • STEP 2:popup の「ログを消す」を押し、popup を閉じて1分ほど待つ
  • STEP 3:popup を開き、「Service Worker にメッセージを送る」を2回押す
  • STEP 4:popup を閉じて1分ほど待つ
  • STEP 5:popup を開き、「Service Worker にメッセージを送る」を1回押す

Service Worker は拡張機能を読み込んだときにも起動するため、STEP 2 で1分待ち、いったん停止させています。

実行結果は以下のとおりです。

14:20:01 [popup] 開いた
14:20:03 [SW] 起動した
14:20:03 [SW] メッセージを受信した(count: 1)
14:20:05 [SW] メッセージを受信した(count: 2)
14:21:10 [popup] 開いた
14:21:12 [SW] 起動した
14:21:12 [SW] メッセージを受信した(count: 1)

STEP 5 のメッセージで「起動した」がもう一度記録され、count が 1 に戻っています。STEP 4 で待っている間に Service Worker が停止し、グローバル変数の値が消えたためです。

STEP 3 から STEP 5 までの Service Worker の状態を表した図です。停止までの30秒は公式ドキュメントの値です。

image.png

Service Worker が停止する条件

Chrome は通常、以下のいずれかに当てはまると Service Worker を停止します。

  • イベントの受信も拡張機能の API 呼び出しもない状態が30秒続く場合
  • 1つのイベントや API 呼び出しの処理に5分以上かかる場合
  • fetch() のレスポンスが届くまでに30秒以上かかる場合

停止した Service Worker は、イベントが届くと再び起動し、スクリプトを最初から実行します。STEP 3 と STEP 5 では、メッセージが届いたことで Service Worker が起動し、トップレベルの log('SW', '起動した') が実行されてから onMessage のリスナーが呼ばれています。

公式ドキュメントでは、Service Worker が停止するとグローバル変数は失われるため、値は chrome.storage などに保存するよう説明されています。

3つの部品の寿命の比較

部品 始まるとき 終わるとき
content script 対象のページを読み込んだとき ページを再読み込みしたとき
popup popup を開いたとき popup を閉じたとき
Service Worker 拡張機能を読み込んだとき、停止中にイベントを受け取ったとき 30秒間イベントも API 呼び出しもなかったときなど

3つの部品はどれも、グローバル変数の値を保ち続けられません。

寿命を前提にした書き方

実験で確かめた寿命と公式ドキュメントの説明から、実装では以下の点を押さえておく必要があります。

  • 3つの部品はどれも終わるとグローバル変数の値が消えるため、残しておきたい値は chrome.storage に保存する
  • 停止中の Service Worker に届いたイベントを取りこぼさないように、イベントリスナーは Promise やコールバックの中ではなく、スクリプトのトップレベルに書く
  • Service Worker が停止すると実行中のタイマーも消えるため、時間をおいて実行する処理や定期的な処理には、setTimeout や setInterval ではなく、Chrome が時間を管理する chrome.alarms を使う

まとめ

content script はページを再読み込みしたとき、popup は閉じたとき、Service Worker は何もしない状態が30秒続いたときに止まります。どの部品も動き続けるとは限らないため、残しておきたい値は chrome.storage に保存しておく必要があります。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?