止まってしまった時計の針を動かしたい
この記事は Cybozu Frontend Advent Calendar 2023 の 15 日目の記事です。
- 14 日目はこちら → 主要ブラウザでサポートされつつある WasmGC とは何なのか
こんにちは。ryounasso です。
ある日、ちょっとした興味で、フロントエンドで処理を行うカウントダウンタイマーを実装しました。
実際に、動かして使おうとしました。
何秒か後に、経過した時間を確認してみると……
動いていなかったのです…… 😭
なぜ動かなかったのか
このタイマーの処理は、フロントエンド側で setInterval
関数を用いて実装しています。
setInerval
関数は、タブがバックグラウンドになると、ブラウザがリソースの節約のために、実行を遅らせたり、停止させたりするそうです。
そのため、別タブに移動した際に setInterval
が停止して、タイマーが動いてなかったのです。
これは Web Worker を使うことで解決できます。
Web Worker とは
Web Worker とは、JavaScript のコードをバックグラウンドで実行できる API のことです。
通常、JavaScript はシングルスレッドで実行されますが、Web Worker を使うことで、マルチスレッドで処理を行うことができます 。
これにより、重い処理や時間のかかる処理をメインスレッドから分離して、バックグラウンドで実行できます 。
本来は、パフォーマンス向上のために用いられるかもしれませんが、setInterval
をバックグラウンドで処理するようにすることで、このタイマーが止まる問題を解決することができます。
Web Worker の使い方
Web Worker のざっくりとした使い方は以下の通りです。
- メインスレッドで
Worker
オブジェクトを生成します。 - メインスレッドと Web Worker は、
postMessage
とonmessage
イベントハンドラまたは、message
イベントを使って、任意のオブジェクトや値をやり取りします。 - メインスレッドから
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 を用いた実装にすることで……
無事動いてくれるようになりました。
よかったです 😭
参考記事
- https://www.wantedly.com/companies/wantedly/post_articles/439961
- https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API
▼Cybozu Frontend Advent Calendar はこちら
Discussion