Manifest V3でbackgroundの前提が変わった
Chrome拡張を作っていて最初につまずきやすいのが、Manifest V3(MV3)でのbackground処理の扱いです。Manifest V2まではbackground.htmlのような「常時起動しているページ」を1枚裏で持たせておくことができ、そこでsetIntervalを回しておけば延々とポーリングし続けるようなことが簡単にできました。
ところがMV3ではこの永続ページが廃止され、background処理は「service worker」というイベント駆動の仕組みに置き換わっています。service workerはイベントが来たときだけ起動し、何もイベントがなければ数十秒ほどで自動的に終了(terminate)してしまいます。常に生きている前提のコードを書くと、ここで静かに壊れます。
setIntervalが効かなくなる
一番よくあるハマりどころが、background.js内でsetIntervalを使って定期処理を組んでしまうケースです。
// これはMV3では動作が保証されない
setInterval(() => {
checkForNewMessages();
}, 30000);
service workerが起動している間はタイマーも動きますが、数十秒操作がないとservice worker自体が終了し、それと同時にsetIntervalで仕込んだタイマーも消えてしまいます。次に何らかのイベントでservice workerが再起動したとき、setIntervalはもう一度最初から呼び直さない限り復活しません。実装した本人は「一応動いているように見える」のでテスト中は気づきにくく、リリース後にしばらく放置していたら通知が全然来ない、という形で発覚しがちなバグです。
chrome.alarmsで最短30秒周期の起床にする
MV3で定期処理をやりたい場合、正攻法はchrome.alarms APIです。これはOS側のアラーム機構を使ってservice workerを指定した間隔で確実に起こしてくれる仕組みで、service workerが休止していても指定時刻になればChromeがきちんと叩き起こしてくれます。
注意点として、chrome.alarmsの最短周期は30秒です。それより短い間隔を指定しても30秒に丸められます。数秒おきのリアルタイム処理には向いていませんが、「一定間隔でサーバーを確認する」程度のポーリング用途には十分です。
まずmanifest.jsonでalarms権限とservice workerを宣言します。
{
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": ["alarms", "storage"]
}
background.js側では、拡張のインストール時にアラームを登録しておきます。
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("poll-check", { periodInMinutes: 0.5 });
});
periodInMinutes: 0.5が30秒周期にあたります。これでChromeが30秒ごとにservice workerを起こし、chrome.alarms.onAlarmイベントを発火してくれるようになります。
service workerは状態を保持できない
ここで見落としがちなのが、service workerはグローバル変数に状態を持たせておいても、休止のたびに消えてしまう点です。前回のチェックでどこまで処理したか、というカーソルやタイムスタンプをただの変数に入れていると、次に起こされたときには初期化された状態からスタートしてしまいます。
これを避けるには、保持したい状態は毎回chrome.storage.localに書き込んでおき、起こされるたびにそこから読み直す、という設計にする必要があります。service worker自身は「状態を持たない実行環境」だと割り切り、状態の置き場所は必ずstorageに寄せるということです。
実例:一定間隔でサーバーの新着を確認する
自作のChrome拡張「QR Send」(https://qrsend.app)でも、この構成をそのまま使っています。スマホから送られてきたURLを中継サーバー経由で受け取る仕組みで、PC側の拡張機能がservice workerとして30秒おきに起こされ、新着がないかを確認し、あれば新しいタブとして開く、という流れです。
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== "poll-check") return;
const { lastCheckedAt } = await chrome.storage.local.get("lastCheckedAt");
const since = lastCheckedAt || 0;
const res = await fetch(`https://example.com/messages?since=${since}`);
const messages = await res.json();
for (const message of messages) {
chrome.tabs.create({ url: message.url });
}
await chrome.storage.local.set({ lastCheckedAt: Date.now() });
});
ポイントは、アラームが発火するたびにchrome.storage.localから前回チェック時刻を読み直し、処理が終わったら次回のために書き戻しているところです。service worker自体がその間に何度休止と再起動を繰り返していても、状態はstorageの中にあるので影響を受けません。
通知を出したい場合はchrome.notificationsを、新着があったことをバッジで示したい場合はchrome.action.setBadgeTextを、同じonAlarmリスナーの中に足していくだけで済みます。
まとめ
- MV3のbackgroundは常時起動の永続ページではなく、イベント駆動のservice workerに変わり、数十秒で自動終了する
setIntervalはservice workerの休止とともに消えるため、定期処理には使えないchrome.alarmsを使えば、休止していても指定間隔でChromeがservice workerを起こしてくれる。ただし最短周期は30秒- service workerはグローバル変数で状態を保持できないため、必要な状態は
chrome.storage.localに永続化し、起こされるたびに読み直す - 「一定間隔でサーバーの新着を確認して通知やタブを開く」というポーリング処理は、この
chrome.alarms+chrome.storageの組み合わせで十分に実現できる


コメント