wasm-pack + WebWorker + react-scripts


前提

  • 重い計算を Rust + WebAssembly でやる
  • WebAssembly のパッケージは wasm-pack でビルドする
  • WebAssembly は WebWorker で動かす
  • アプリは React で書く
  • アプリの設定を頑張りたくないので react-scripts (create-react-app) を使う

react-app-rewired

react-scripts では、WebWorker と WebAssembly のロードができないので設定する必要がある。
react-scripts で eject せずに設定を追加するために react-app-rewired を使う。

worker-loader では WebWorker から WebAssembly を import できなかったので、workerize-loader を使う。

こんな感じで config-overrides.js を書く。

config-overrides.js
const path = require("path");

module.exports = function override(config, env) {
  config.module.rules.push({
    test: /\.worker\.js$/,
    use: { loader: "workerize-loader" },
  });

  const wasmExtensionRegExp = /\.wasm$/;

  config.resolve.extensions.push(".wasm");

  config.module.rules.forEach((rule) => {
    (rule.oneOf || []).forEach((oneOf) => {
      if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
        // Make file-loader ignore WASM files
        oneOf.exclude.push(wasmExtensionRegExp);
      }
    });
  });

  // Add a dedicated loader for WASM
  config.module.rules.push({
    test: wasmExtensionRegExp,
    include: path.resolve(__dirname, "src"),
    use: [{ loader: require.resolve("wasm-loader"), options: {} }],
  });

  return config;
};

これで拡張子が .worker.js のファイルを WebWorker として読み込めるようになる。

package.jsonscripts を以下のように書いておく。

package.json
  "scripts": {
    "build": "react-app-rewired build",
    "start": "react-app-rewired start"
  },

WebWorker の実装

こんな感じで WebWorker を実装する。

example.worker.js
export const twice = async (v) => {
  const { twice } = await import("example");
  return twice(v);
};

example は wasm-pack で作られたパッケージで、dynamic import で読み込む。

workerize-loader では、WebWorkerで呼び出せる関数を named export する。
async/await が使える。

アプリの実装

いい感じにアプリ側を実装する。
workerize-loader で import したモジュールは関数になっていて、呼び出すと関数を取り出すことができる。
関数は Promise を返すようになっている。

App.js
import React, { useState } from "react";
import worker from "./hoge.worker";

const { twice } = worker();

const App = () => {
  const [value, setValue] = useState(1);
  return (
    <div>
      <button
        onClick={() => {
          twice(value).then((result) => {
            setValue(result);
          });
        }}
      >
        click me
      </button>
      <p>{value}</p>
    </div>
  );
};

export default App;

実用例

凸包を計算して描画するプログラムを書いてみた。