🕰️

止まってしまった時計の針を動かしたい

2023/12/15に公開

この記事は Cybozu Frontend Advent Calendar 2023 の 15 日目の記事です。

こんにちは。ryounasso です。

ある日、ちょっとした興味で、フロントエンドで処理を行うカウントダウンタイマーを実装しました。

Timer

実際に、動かして使おうとしました。

何秒か後に、経過した時間を確認してみると……

NotWorkTimer

動いていなかったのです…… 😭

なぜ動かなかったのか

このタイマーの処理は、フロントエンド側で setInterval 関数を用いて実装しています。
setInerval 関数は、タブがバックグラウンドになると、ブラウザがリソースの節約のために、実行を遅らせたり、停止させたりするそうです。
https://stackoverflow.com/questions/6032429/chrome-timeouts-interval-suspended-in-background-tabs

そのため、別タブに移動した際に setInterval が停止して、タイマーが動いてなかったのです。
これは Web Worker を使うことで解決できます。

Web Worker とは

Web Worker とは、JavaScript のコードをバックグラウンドで実行できる API のことです。

https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API

通常、JavaScript はシングルスレッドで実行されますが、Web Worker を使うことで、マルチスレッドで処理を行うことができます 。

これにより、重い処理や時間のかかる処理をメインスレッドから分離して、バックグラウンドで実行できます 。

本来は、パフォーマンス向上のために用いられるかもしれませんが、setInterval をバックグラウンドで処理するようにすることで、このタイマーが止まる問題を解決することができます。

Web Worker の使い方

Web Worker のざっくりとした使い方は以下の通りです。

  1. メインスレッドで Worker オブジェクトを生成します。
  2. メインスレッドと Web Worker は、postMessageonmessage イベントハンドラまたは、 message イベントを使って、任意のオブジェクトや値をやり取りします。
  3. メインスレッドから terminate メソッドを実行するか、Web Worker から close メソッドを呼び出すことで、Web Worker を終了させることができます。

サンプルコード

React を使ったサンプルコードは以下の通りです。

// worker.js

self.addEventListener("message", (e) => {
  let remainTime = e.data.time;
  const intervalId = setInterval(() => {
    remainTime--;
    if (remainTime <= 0) {
      clearInterval(intervalId);
      self.postMessage({ remainTime });
    } else {
      self.postMessage({ remainTime });
    }
  }, 1000);
});

メインスレッド

// index.js

import { useState, useRef, useEffect } from "react";

export default function Home() {
  const [time, setTime] = useState(1800);
  const [isCounting, setIsCounting] = useState(false);
  const workerRef = useRef(null);

  useEffect(() => {
    workerRef.current = new Worker(
      new URL("./worker/worker.js", import.meta.url)
    );

    workerRef.current.addEventListener("message", (e) => {
      if (e.data.remainTime === 0) {
        setTime(e.data.remainTime);
        alert("Time is up!");
        setTime(1800);
        setIsCounting(false);
      } else {
        setTime(e.data.remainTime);
      }
    });

    return () => {
      workerRef.current.terminate();
    };
  }, []);

  const startTimer = () => {
    setIsCounting(!isCounting);
    workerRef.current.postMessage({ time });
  };

  return (
    <div>
      <button onClick={startTimer} disabled={isCounting}>
        Start Timer
      </button>
      {time && <p>{convertToTime(time)}</p>}
    </div>
  );
}

注意点

  • React で Web Worker を使用する際は、worker は ref で保持した方が良いです。
    ref であれば、レンダリング間で値を保持してくれますが、通常の変数の場合、再レンダリングのたびに初期化されてしまい、思った挙動をしてくれません。
  • Web Worker の数が増えすぎると、パフォーマンスやメモリの消費が増える可能性があります。不必要になる Worker は終了させることを忘れないようにしましょう。

もう一度、動けタイマーよ

Web Worker を用いた実装にすることで……

WebWorkerTimer

無事動いてくれるようになりました。
よかったです 😭

参考記事

▼Cybozu Frontend Advent Calendar はこちら

https://adventar.org/calendars/9255

サイボウズ フロントエンド

Discussion