🪔

Next.jsでお手軽にWeb-Workerを呼び出す

commits5 min read

Next.jsでWeb-Workerを簡単に使うには

前回の記事で書きましたが、Next.jsには最初からWeb-Workerを読み込むためのWebPack設定が入っているので、そのまま利用することが可能です
しかしWeb-Workerとして書いたスクリプトを読み込む設定が入っているだけで、それ以外のサポートはありません
機能を利用するには、コード上でそれなりに面倒な手順を記述する必要があります

前回の記事

Next.jsでWeb-Workerを呼び出す

サンプル置き場

https://github.com/SoraKumo001/next-worker2
https://next-worker2.vercel.app/

Web-Workerを使う上での面倒なポイント

  • Worker機能のファイルを別に作成する必要がある
    インライン機能はあるものの、それはそれで面倒です
  • メインスレッドとWorkerのやりとりは、postMessageとaddEventListenerを経由しなければならない
    ちょっとしたデータのやりとりをしようにも、この手続きでコードの見通しが悪くなります
  • データのやりとりを行うときにTypeScriptに型を付けようとすると、Worker側とメインスレッド側でそれぞれ書かなければならない
    二重に同じようなコードを書く手間がかかるのは避けたいところです

解決方法の模索

postMessageとaddEventListenerを消し去って、Worker側で作成した関数をメインスレッド側でそのまま呼び出せるようにすれば良いのです

なんという名案!

問題はあっさり解決しました

作ったもの

web-workerを簡単に使うためのライブラリをnpmに登録しました
https://www.npmjs.com/package/worker-lib

サンプルコード

Worker side processing

5つの関数を作成し、initWorkerでWorkerとして使える状態にします
そして型情報のみexportしておきます

  • src/libs/worker-test.ts
import { initWorker } from "worker-lib";

const add = (a: number, b: number) => {
  for (let i = 0; i < 1000000000; i++); //Overload unnecessarily
  return a + b
}
const add2 = (a: string, b: string) => {
  for (let i = 0; i < 1000000000; i++); //Overload unnecessarily
  return a + b
}
const sub = (a: number, b: number) => {
  for (let i = 0; i < 1000000000; i++); //Overload unnecessarily
  return a - b
}
const mul = (a: number, b: number) => {
  for (let i = 0; i < 1000000000; i++); //Overload unnecessarily
  return a * b
}

const error = (a: number, b: number) => {
  for (let i = 0; i < 1000000000; i++); //Overload unnecessarily
  throw new Error("throw")
  return  a + b;
}

// Initialization process to make it usable in Worker.
const map = initWorker({ add, add2, sub, mul, error })
// Export only the type
export type WorkerTest = typeof map

Processing for the user(Next.js)

createWorkerでworker-testを読み込みます
その際、WorkerTestという名前でexportしていた型情報を渡します
また、最大スレッド数をここで設定しておくと、上限を超えた呼び出しはプールされ、実行可能になるまで待機状態になります

戻り値としてWorker実行用の関数が返ってくるので、あとはそれを使ってWorkerを呼び出せます

  • src/pages/index.tsx
import { useState } from "react";
import { createWorker } from "worker-lib";
import type { WorkerTest } from "../libs/worker-test";

// Create an instance to execute the Worker
// execute("function name",... parameter) to start the Worker
const execute = createWorker<WorkerTest>(
  () => new Worker(new URL("../libs/worker-test", import.meta.url)),
  5 // Maximum parallel number
);

const Page = () => {
  const [values, setValues] = useState<(number | string)[]>([]);
  const [a, setA] = useState(300);
  const [b, setB] = useState(100);
  return (
    <div>
      <form>
        <input name="a" value={a} onChange={(e) => setA(Number(e.currentTarget.value))} />
        <input name="b" value={b} onChange={(e) => setB(Number(e.currentTarget.value))} />
        <button
          type="button"
          onClick={async () => {
            const index = values.length;
            setValues([...values, "running"]);
            //Calling a Worker
            const result = await execute("add", a, b);
            setValues((values) => values.map((v, i) => (i === index ? result : v)));
          }}
        >
          Add
        </button>
        <button
          type="button"
          onClick={async () => {
            const index = values.length;
            setValues([...values, "running"]);
            //Calling a Worker
            const result = await execute("add2", String(a), String(b));
            setValues((values) => values.map((v, i) => (i === index ? result : v)));
          }}
        >
          Add(String)
        </button>
        <button
          type="button"
          onClick={async () => {
            const index = values.length;
            setValues([...values, "running"]);
            //Calling a Worker
            const result = await execute("sub", a, b);
            setValues((values) => values.map((v, i) => (i === index ? result : v)));
          }}
        >
          Sub
        </button>
        <button
          type="button"
          onClick={async () => {
            const index = values.length;
            setValues([...values, "running"]);
            //Calling a Worker
            const result = await execute("mul", a, b);
            setValues((values) => values.map((v, i) => (i === index ? result : v)));
          }}
        >
          Mul
        </button>
        <button
          type="button"
          onClick={async () => {
            const index = values.length;
            setValues([...values, "running"]);
            //Calling a Worker
            const result = await execute("error", a, b).catch((e) => e);
            setValues((values) => values.map((v, i) => (i === index ? result : v)));
          }}
        >
          Error
        </button>
      </form>
      {values.map((v, index) => (
        <div key={index}>{v}</div>
      ))}
    </div>
  );
};
export default Page;
const result = await execute("add", a, b);

以上のような形で使います
名前に対応した型情報が自動的に割り当てられるので、パラメータの型が違うとその時点でエラーとなります
戻り値に対しても型情報が自動的に設定されます

まとめ

Worker用の関数の作成と利用が直感的になったので、かなり敷居が下がったのではないでしょうか
ただ、Webシステムは非同期で大概の処理が何とかなってしまうのが実情なので、マルチスレッドの使い道を探す方が大変かもしれません

GitHubで編集を提案

Discussion

ログインするとコメントできます