💨

ReactでWebWorkerを活用するカスタムフック

2024/10/30に公開

React のカスタムフックと WebWorker を統合することで、UX を向上させることができるので、その方法を紹介します。

WebWorker とは

まず、JavaScript はシングルスレッドで動作しているので、基本的に全ての処理を、一箇所で順番に処理をしています。
このスレッドをメインスレッドと呼ぶのですが、そのメインスレッドで重い処理を行うと、画面が固まるなどの問題が発生します。
そういった場合に、WebWorker を使うとこの問題を解消できます。

WebWorker とは何かというと、メインスレッドとは独立したスレッドで JavaScript を実行するための仕組みです。
メインスレッドとは別のスレッドで動作するので、重い処理を行ってもメインスレッドが固まることはありません。(= 画面が固まることはない)

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

WebWorker を使用しない場合

WebWorker なし

計算中はメインスレッドが占有されるので、他の操作ができなくなります。

WebWorker を使用した場合

WebWorker あり

計算中はメインスレッドが占有されないので、他の操作ができます。

カスタムフックで WebWorker を使う

では、WebWorker を React で使う際に有用な一例を紹介します。
それは、WebWorker をカスタムフックとして使うことです。

結論から言うと、以下のようになります。

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

type WorkerFunction<T, U> = (input: T) => U;

// useWorkerは、WebWorkerを使用して非同期処理を行うカスタムフック
export function useWorker<T, U>(workerFunc: WorkerFunction<T, U>, input: T) {
  // 結果を保持するためのstate
  const [result, setResult] = useState<U | null>(null);
  // Workerインスタンスを保持するためのref
  const workerRef = useRef<Worker | null>(null);

  useEffect(() => {
    // WebWorker用のスクリプトをBlobとして作成
    const blob = new Blob(
      [
        `
          self.onmessage = async (e) => {
            const func = ${workerFunc.toString()};
            const result = func(e.data);
            self.postMessage(result);
          };
        `,
      ],
      { type: "application/javascript" }
    );

    // BlobからWorkerを作成
    const worker = new Worker(URL.createObjectURL(blob));
    workerRef.current = worker;

    // Workerからのメッセージを受け取ったときに結果をstateにセット
    worker.onmessage = (e) => {
      setResult(e.data);
    };

    // Workerに入力データを送信
    worker.postMessage(input);

    // コンポーネントのアンマウント時にWorkerを終了
    return () => {
      worker.terminate();
    };
  }, [workerFunc, input]);

  // 計算結果を返す
  return result;
}

コードの説明

このカスタムフック useWorker は、WebWorker を使用して非同期処理を行うためのものです。以下にその動作を説明します。

  1. useStateuseRef の使用:

    • useState を使用して、WebWorker の計算結果を保持する result を管理します。
    • useRef を使用して、WebWorker のインスタンスを保持します。これにより、コンポーネントの再レンダリング時に新しい Worker を作成することを防ぎます。
  2. useEffect の使用:

    • useEffect フックは、workerFuncinput が変更されるたびに実行されます。
    • WebWorker 用のスクリプトを Blob として作成し、それを使用して新しい Worker を生成します。
    • Worker からのメッセージを受け取ったときに、setResult を使用して結果を更新します。
    • コンポーネントがアンマウントされるときに Worker を終了します。
  3. WebWorker の作成と使用:

    • workerFunc を文字列として Blob に埋め込み、WebWorker 内で実行します。
    • input を Worker に送信し、計算を非同期で行います。
    • 計算が完了すると、結果がメインスレッドに送信され、result に保存されます。

使用例

以下は、このカスタムフック useWorker を使用した例です。
(ここでは仮にフィボナッチ数列を計算する関数を使用しています)

import React from "react";
import { useWorker } from "./useWorker";

// 重い計算を行う関数
const heavyComputation = (num: number) => {
  const fibonacci = (n: number): number => {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  };

  const fibSequence = [];
  for (let i = 0; i <= num; i++) {
    fibSequence.push(fibonacci(i));
  }
  return fibSequence;
};

const WorkerExample: React.FC = () => {
  const inputNumber = 20;
  const result = useWorker(heavyComputation, inputNumber);

  return (
    <div>
      <h1>WebWorkerを使用した計算結果</h1>
      <p>入力値: {inputNumber}</p>
      <p>計算結果: {result !== null ? result.join(", ") : "計算中..."}</p>
    </div>
  );
};

export default WorkerExample;

この例では、heavyComputation という計算負荷の高い関数を useWorker フックに渡し、WebWorker を利用して非同期的に計算を実行しています。計算の結果は、コンポーネント内で result として表示されます。

まとめ

カスタムフックにおける WebWorker の活用方法について説明しました。

これまで WebWorker をあまり使用してこなかったのですが、今回の実装を通じて多くの新しい知識を得ることができました。

個人的には、WebWorker を活用することで、特に計算が重いアプリにおいて、ユーザー体験をかなり向上させられると感じています。これまで WebWorker をあまり使っていなかった人も、このカスタムフックを使うことでその便利さを実感できると思います。

備考

JSON 形式や Map や Set などの特殊なデータ構造を Web Worker で利用するには、これらの構造をシリアライズ・デシリアライズするための追加の処理が必要です。
自分で実装しても良いのですが、結構めんどくさいので個人的には下記のようなライブラリを使うのがおすすめです。

https://github.com/flightcontrolhq/superjson
https://github.com/WebReflection/flatted

GitHubで編集を提案

Discussion