create-react-app(TypeScript)で作成したアプリにWeb Workerを導入する方法


はじめに

以前作成したQRコード認識Reactコンポーネントを格好よくするため、上下に移動する「緑色のバー」を追加しました(CSS animation)。

※ ↓ 画像なのでわかりづらいですが「緑色のバー」が上下します。

ところが、バーの動きがガクガクになり、スムーズにアニメーションしてくれません。
QRコードの認識処理が描画処理をブロックするためです。(タイマーで定期的に認識処理を実行)

こういう場合はWeb Workerを利用すれば、バックグラウンドで認識処理行うことができます。
ところがcreate-react-appで作成したプログラムには、簡単には導入できないようです。

  • create-react-appがWeb Workerをサポートしていない

  • Web Workerは.jsファイルを要求するため、ejectしてからWebPackを設定する必要がある。

試行錯誤の上、eject無しに動く手段が見つかりました。


comlink-loaderを使うと、容易に導入できます

Web Workerで実行する処理をTypeScriptの通常のメソッドとして書くことができます。
呼び出しも通常の非同期メソッド(postMessageは不要)として呼びだすことができる優れものです。

Web Workerを意識せず、メソッドの呼び出しとして処理できてしまいます!

comlink-loaderの組み込み手順

  • ./src/worker フォルダに下記3ファイルを作成します
ファイル名 説明
custom.d.ts 型定義。worker.tsの型に合わせる(戻り値はPromis<>でラップする)
index.ts workerのインラインローダー。説明を読んでもよくわかりません・・・・
worker.ts Web Workerに実行させる処理(function)定義
/* ./worker/custom.d.ts */
declare module 'comlink-loader!*' {
  class WebpackWorker extends Worker {
    constructor();

    // Add any custom functions to this class.
    // Make note that the return type needs to be wrapped in a promise.
    processData(data: ImageData): Promise<QRCode>;
  }

  export = WebpackWorker;
}
/* ./worker/index.ts */
// eslint-disable-next-line
import Worker from 'comlink-loader!./worker'; // inline loader

export default Worker;
  • QRコードの認識処理を記載します。
/* ./worker/worker.ts */
import jsqr, { QRCode } from 'jsqr';

export function processData(data: ImageData): QRCode {
  // Process the data without stalling the UI
  const qr = jsqr(data.data, data.width, data.height);
  if (qr) {
    console.log(qr.data);
    return qr;
  }
  return null;
}
  • 利用側ソース

Workerを生成して、Promiseを返す非同期メソッドとして呼び出すだけです。
(workerはレンダリング毎に生成されるのを防ぐためuseMemo()でキャッシュしています)

PostMessage()を使わず、普通のメソッドとしてWeb Workerが呼び出せてしまいます。

/* ./QRReader.tsx */
const QRReader: React.FC<QRReaderProps> = (props) => {
  const worker = useMemo(() =>  new Worker(), [])

  // ~~~ 途 中 略 ~~~

  timerId.current = setInterval(() => {
    context.drawImage(video.current, 0, 0, width, height);
    const imageData = context.getImageData(0, 0, width, height);
    worker.processData(imageData).then(qr => {
    if (qr) {
      console.log(qr.data);
      if (props.showQRFrame) {
        drawRect(qr.location.topLeftCorner, qr.location.bottomRightCorner);
      }
      if (props.onRecognizeCode) props.onRecognizeCode(qr);               
    }
    });
  }, props.timerInterval);

create-react-appで作ったアプリケーションとWeb Workerを組み合わせた場合に発生する技術的な課題について

Create React AppでWeb Workerを使うには からの引用です。Web Workerを使うのはかなりしんどいようです。

1. publicフォルダにWorkerのJSファイルを配置して読み込む

Web Workerは.jsファイルを読み込むため、publicフォルダにworker.jsファイルを別途作成して配置しておくか、worker.tsを別途tscでビルドしてpublicフォルダに配置するようにする必要がある。

const worker = new Worker('worker.js');
worker.postMessage(`hoge`);

処理内容をTypeScriptで書きたい、別にビルドするのが面倒なので却下

2. ejectしてからWebPackの設定に worker-loader または worker-plugin を追加する

ejectしたくない、WebPackを直接使いたくないので却下

3. ejectせずに react-app-rewired を使ってWebPackの設定に worker-loader または worker-plugin を追加する。

WebPackを直接使いたくないので却下

4. WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成する。

わからんでもないが、トリッキー過ぎるので却下

Is it possible to use load webworkers? #1277 で上記1.~4.の議論が行われていますが、結論が良く割りませんでした。