komura-c.log

https://blog.komura-c.page/ に移動しました

AngularでWebWorkerを使ったバックグラウンドで生きるSetInterval

JavaScriptでは、一定の間隔で関数やコードスニペットを繰り返し呼び出す用のSetIntervalというWebAPIが使用できます。

setInterval() - Web API | MDN

こちらをログなどのため、スマホのバックグラウンドでも動かしたいという要件があったのですが、

バックグラウンドのタブによる負荷(および関連するバッテリーの使用量)を軽減するために、ブラウザはアクティブでないタブの最小タイムアウト時間を強制します。

アクティブでないタブの最小タイムアウトは 1 秒です。

setTimeout() - Web API | MDN

などの理由でアクティブなタブとそうでないタブで動作が異なることが分かりました。(実際の実装でも、バックグラウンドでの動作が不安定でした)

そのため、WebWorkerを使うことで、バックグラウンドでもアクティブなタブの時のように指定した秒数でタイマーが動くように実装したのでメモします。

WebWorkerとは

Web Worker とは、ウェブアプリケーションにおけるスクリプトの処理をメインとは別のスレッドに移し、バックグラウンドでの実行を可能にする仕組みのことです。時間のかかる処理を別のスレッドに移すことが出来るため、 UI を担当するメインスレッドの処理を中断・遅延させずに実行できるという利点があります。

とあります。

developer.mozilla.org

AngularでのWebWorkerの作り方

Angularでは、次のngコマンドでWebWorkerを生成することができます。

ng generate web-worker workers/set-interval(対象ディレクトリor使うcomponentパス/Worker名)

Angular 日本語ドキュメンテーション

Workerを使ったSetInterval

その後生成されたxx.worker.tsを次のように書き換えます。

// アプリケーション側からpostMessageされたものが送られる
addEventListener('message', ({ data }) => {
  const intervalTime = event.data.intervalTime;
  if (intervalTime) {
  // WorkerスレッドでsetIntervalする
    setInterval(() => {
      postMessage(null);
    }, intervalTime) as unknown as number;
  }
});

そして該当ServiceやComponentから次のように設定します

worker: Worker = null;
// 2分に設定
intervalTime = 2 * 60 * 1000;
intervalId: number = null; // typeof window.setInterval

if (typeof Worker !== 'undefined') {
  // ワーカーを作成
 this.worker = new Worker(new URL('../workers/set-interval.worker', import.meta.url));
// Worker側からpostMessageされたものが送られる
  this.worker.onmessage = () => {
      // 定期的に実行したい関数
      callback();
  };
  // アプリケーション側から2分間隔で送るようにWorkerのSetIntervalを設定
  this.worker.postMessage({intervalTime: this.intervalTime});
} else {
    // Web Workerがサポートされていない時の処理
     this.intervalId = setInterval(
        callback,
        this.intervalTime,
      )
}

こういった実装をすることで、スマホのバックグラウンドやアクティブでないタブの時も、通常の動作のSetIntervalを設定することができます。

注意点としては、Workerは止めるか、Workerが存在するか、タブが閉じられない限り生き続けてしまい、メモリリークなどの原因となる場合があるため、必要がなくなったときは、次のようにworker.terminate();を呼ぶことで終了できます。

    if (this.worker) {
      // terminateでworkerを強制終了、worker自体をnullに
      this.worker.terminate();
      this.worker = null;
    } else if (this.intervalId) {
    // Web Workerがサポートされていない時に通常のsetIntervalをしていたらclearIntervalする
      clearInterval(this.intervalId);
    }

Worker.terminate() - Web API | MDN

まとめ

WebWorkerを使う機会がなく、初めて使いましたがWorkerスレッドでスマホのバックグラウンドでも実行したい動作を統一することや、メインスレッドの負担になるような重い処理を逃すことに使えるというのを実際に体験できました。 参考になれば嬉しいです。

参考

タブがバックグラウンドになってもサボらない setInterval · GitHub