この記事は元々2023年12月22日に個人ブログで公開された内容をQiitaに移行したものです。
Chromeの拡張機能をManifest V3で開発していて困ったことがありました。
「1分おきに特定のwebページにアクセスして状態を確認する」という処理をService workerにやらせようとしたのですが、1分ごとに処理が走るはずが、なぜかループが止まっていました。
そこでService Workerの状態を確認してみると、「Service Worker(無効)」の文字が…。
観察してみると、どうやらService Workerがループ開始から30秒で自動停止してしまっているようです。
chrome.runtime.sendMessage
でService Workerにメッセージを送ると起動するのですが、時間のかかる処理を走らせているとまた無効になります。再びメッセージを送るまで、Service Workerは眠ってしまうんですね。
原因
色々調べてみると、これはManifest V3特有の仕様で、意図的にService Workerが一定時間で停止する設計になっているようです。
以下の Chromium Issue Tracker にて、開発者のやりとりが確認できます。
I’ve seen some Chromium developers saying it’s WAI, but let me disagree: this behavior is WAI only for the web because users visit a lot of sites unpredictably and may open a lot of tabs so the aggressive shutdown of workers is the expected and the correct behavior by default.
However, as for the extensions, there’ll be just a few (or maybe a few dozen in rare cases). Extensions are installed knowingly to extend the browser itself so the browser shouldn’t be as aggressive towards them. Maybe instead of “shoot-on-sight” strategy Chrome could switch to informing the users e.g. show the extension’s process memory and CPU usage in the new extension menu.
要するに、メモリ消費を抑えるために積極的に停止させているというわけですね。
複数の拡張機能が同時にバックグラウンドで動いていたら、そりゃあメモリ食いますもんね…。
とはいえ、Manifest V2では常駐できていたため、多くの開発者から不満の声が上がっています。将来的に何かしら変更があるかも?
対処法
開発側が意図的にService Workerを停止しているのであれば、そもそもService Workerを永続化しなくても良い実装にするのが一番ですよね。
とはいえ、ユースケースによってはやはりService Workerを永続化したい場合があると思います。
例えば私の場合は、「1分おきに特定のwebページにアクセスして状態を確認する」という処理をService Workerにやらせたいです。
拡張機能のポップアップやオプションページでも処理を走らせることはできますが、タブなどを閉じると動作が停止してしまうので、やはりバックグラウンドで動作するService Workerにやらせたいんですよね。
対処法を考えてみました。
対処法1. Manifest V2に戻す(非推奨)
得策とは言えませんが、Manifest V2ならService Workerを永続化するオプションがあったので、それだけで解決します。
しかしManifest V2はすでに非推奨で、いつ使用不可になるのかわからないので、V3の利用を継続すべきでしょう。
対処法2. Offscreenから定期的に空メッセージを送る
Service Workerは、chrome.runtime.sendMessageでメッセージを受け取ると、停止していたとしても再び動作を再開します。
つまり、停止するよりも早い間隔で定期的にchrome.runtime.sendMessageを呼び続ければ、停止を防ぐことができます。
そのメッセージを送り続ける定期処理を、offscreenにやってもらえば良いのです。
※ offscreenは、バックグラウンドでレンダリングされる画面です。通常のページと同じようにhtmlベースで動きますが、通常のページのようにユーザーから操作を行うことはできません。
1. まずoffscreen.html
を準備します。
(スクリプトを動作させたいだけなので、これだけで十分)
<html lang="en">
<script src="offscreen.js"></script>
</html>
2. 次に動作スクリプトを書きます。
// 25秒ごとにchrome.runtime.sendMessageを呼ぶ
setInterval(() => {
chrome.runtime.sendMessage("何でも良いメッセージ");
}, 25 * 1000);
offscreenから25秒ごとにchrome.runtime.sendMessageを呼びます。
(30秒で停止してしまうので、それより短い25秒)
ちなみにもしoffscreenをReactで実装するならこんな感じ↓
export function Offscreen() {
React.useEffect(() => {
keepServiceWorker();
}, []);
return null;
};
async function keepServiceWorkerAlive() {
setInterval(() => {
chrome.runtime.sendMessage("何でも良いメッセージ");
}, 25 * 1000);
}
3. background.js(Service Worker)
async setUpOffscreen() {
await chrome.offscreen.createDocument({
url: browser.runtime.getURL('offscreen.html'),
reasons: ['BLOBS'],
justification: 'To keep service worker',
});
}
// 拡張機能インストール時にoffscreen作成
chrome.runtime.onInstalled.addListener(() => {
setUpOffscreen();
});
// ブラウザ起動時にoffscreen作成
chrome.runtime.onStartup.addListener(() => {
setUpOffscreen();
});
これで拡張機能インストール時やブラウザ起動時にoffscreenが作成され、そのoffscreenが25秒ごとにsendMessageを呼び続けるので、Service Workerが停止することはありません。