【Chrome拡張機能開発】Manifest V3でService workerが自動停止してしまう問題の原因と対処法

:::note
この記事は元々**2023年12月22日**に個人ブログで公開された内容をQiitaに移行したものです。
:::

Chromeの拡張機能をManifest V3で開発していて困ったことがありました。

「１分おきに特定のwebページにアクセスして状態を確認する」という処理をService workerにやらせようとしたのですが、１分ごとに処理が走るはずが、なぜかループが止まっていました。

そこでService Workerの状態を確認してみると、「Service Worker（無効）」の文字が…。

![ea529f0aaa8d49c6a87352c71b9c2223.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981597/71252bab-1d3f-4c77-99b1-6ba59debfed0.png)


観察してみると、どうやらService Workerがループ開始から**30秒で自動停止**してしまっているようです。

`chrome.runtime.sendMessage`でService Workerにメッセージを送ると起動するのですが、時間のかかる処理を走らせているとまた無効になります。再びメッセージを送るまで、Service Workerは眠ってしまうんですね。

## 原因

色々調べてみると、これは**Manifest V3特有の仕様**で、意図的にService Workerが一定時間で停止する設計になっているようです。

以下の [Chromium Issue Tracker](https://bugs.chromium.org/p/chromium/issues/detail?id=1152255) にて、開発者のやりとりが確認できます。

> 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を永続化したい場合があると思います。

例えば私の場合は、「１分おきに特定の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ベースで動きますが、通常のページのようにユーザーから操作を行うことはできません。

![sendMessage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981597/2b5bea74-6929-4ff4-9807-1f6064f33432.jpeg)

**1. まず`offscreen.html`を準備します。**
（スクリプトを動作させたいだけなので、これだけで十分）

```html:offscreen.html
<html lang="en">
  <script src="offscreen.js"></script>
</html>
```

**2. 次に動作スクリプトを書きます。**

```javascript:offscreen.js
// 25秒ごとにchrome.runtime.sendMessageを呼ぶ
setInterval(() => {
  chrome.runtime.sendMessage("何でも良いメッセージ");
}, 25 * 1000);
```

offscreenから25秒ごとにchrome.runtime.sendMessageを呼びます。
（30秒で停止してしまうので、それより短い25秒）

ちなみにもしoffscreenをReactで実装するならこんな感じ↓

```react:offscreen.tsx
export function Offscreen() {
  React.useEffect(() => {
    keepServiceWorker();
  }, []);

  return null;
};

async function keepServiceWorkerAlive() {
  setInterval(() => {
    chrome.runtime.sendMessage("何でも良いメッセージ");
  }, 25 * 1000);
}
```

**3. background.js（Service Worker）**

```javascript:background.js
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が停止することはありません。