TypeScript 5 + webpack 5でWebWorker

## 先に結論だけ

[webpack5](https://webpack.js.org/)では、プラグインの導入なしにWebWorker用のJavaScriptが[書き出せます](https://webpack.js.org/guides/web-workers/)。WebWorkerの導入によって処理時間が短縮されるケースは限られますが、フレーム落ちを防いでユーザー体験を向上させます。

最小限の構成でWebWorkerを導入するサンプルを作成しました。こちらのリポジトリも参照してください。

https://github.com/MasatoMakino/minimal-webworker-webpack-typescript

## はじめに

この記事は、webpack5でのWebWorkerの導入方法を記録、共有するものです。

### 対象とする読者

- フロントエンド開発者
- webpackを使ったことがある
- TypeScriptを使ったことがある
- WebWorkerを使ったことがないが、興味がある

### 想定する環境

- Node.js v18.16.0
- webpack v5.84.1
- TypeScript v5.0.4

:::note warn
対象となるパッケージのメジャーバージョンが変わると、記事の内容がそのまま適用できないかもしれません。記事を読む前に、お手元の環境をご確認ください。
:::

## WebWorkerとは

[WebWorker](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API)とは、JavaScriptでマルチスレッド処理を実現するための仕組みです。

### マルチスレッド

ワーカーは[メインスレッド](https://developer.mozilla.org/ja/docs/Glossary/Main_thread)とは切り離された、別スレッドでJavaScriptを実行します。JavaScriptはシングルスレッドで動作するため、CPUバウンドな処理を行うとスレッドがブロックされてしまいます。ワーカーは別スレッドで実行されるため、処理中もメインスレッドは平常通りに画面を描画します。

### メッセージとメモリの移転

ワーカーは別スレッドで実行されるため、メインスレッド上のメモリを読み込めません。WebWorkerに変数を渡すには、メッセージと移転の2つの方法があります。

#### メッセージ

WebWorkerへメモリを渡すには、[メッセージを送る](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers#%E5%B0%82%E7%94%A8%E3%83%AF%E3%83%BC%E3%82%AB%E3%83%BC%E3%81%A8%E3%81%AE%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AE%E3%82%84%E3%82%8A%E3%81%A8%E3%82%8A)方法があります。Workerの[`postMessage`メソッド](https://developer.mozilla.org/ja/docs/Web/API/Worker/postMessage)を使って、メインスレッドとワーカーは相互にメッセージを送ります。

postMessageメソッドで転送可能なのは[構造化複製アルゴリズム](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)でコピー可能なオブジェクトに限られます。たとえば、関数をもったオブジェクトやDOMノードは転送できません。

この方法では、メッセージを送信するたびにメモリのコピーが作成されます。巨大なオブジェクトを転送する場合、サイズに応じて処理時間がかかります。

#### 移転

JavaScriptには[移転可能オブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Transferable_objects)というオブジェクトがあります。このオブジェクトはスレッド間で所有権を移転できます。移転されたオブジェクトは、元のスレッドからはアクセスできなくなり、アクセスするとエラーが発生します。

移転はWorkerの`postMessage`メソッドに[第二引数](https://developer.mozilla.org/ja/docs/Web/API/Worker/postMessage#%E5%BC%95%E6%95%B0)のtransferオプションを与えることで設定できます。

```js
const myWorker = new Worker("myWorker.js");
const myBuf = new ArrayBuffer(8);

myWorker.postMessage(myBuf, [myBuf]);
```

transferオプションは、移転可能オブジェクトの配列です。

移転はメモリのコピーを作成しないため高速です。しかし転送可能なオブジェクトが限られるため、メインスレッドとWorkerの双方で処理内容を検討、修正する必要があります。

### スクリプトのコンパイル

WebWorker用のスクリプトは別ファイルに保存され、各ワーカーが起動時にネットワークから読み込みます。ワーカーは起動するたびにそのスクリプトをコンパイルします。

数百KBを超えるような、巨大なWorkerをたくさん起動するとコンパイルの時間が並列処理による恩恵を上回ってしまいます。ワーカーのフットプリントはできるだけ小さく抑えましょう。

### WebWorkerに向いている処理

- CPUバウンドな処理
- DOMを操作しない
- 他の処理に影響されず、並列処理が可能
- メモリ転送量が少ない
- 巨大なパッケージの読み込みを必要としない

これらの条件をクリアできている場合、WebWorkerの導入を検討しましょう。

## webpack5でWebWorker

webpack5では、プラグインなしに本体の機能のみでWebWorker用スクリプトのバンドルと書き出しができます。

### 基本構成

https://webpack.js.org/guides/web-workers/


この例が、webpack5でのWebWorkerの最小構成です。

```js
new Worker(new URL('./worker.js', import.meta.url));
```

[new Worker](https://developer.mozilla.org/ja/docs/Web/API/Worker/Worker)コンストラクターの引数に文字列ではなく[URLインターフェイス](https://developer.mozilla.org/ja/docs/Web/API/URL)を渡します。URLインターフェイスの第二引数には[import.meta.url](https://webpack.js.org/api/module-variables/#importmetaurl)を渡します。webpackはimport.meta.urlをもとにWorker用スクリプトのパスを解決して出力します。

### TypeScriptの導入

TypeScriptと[ts-loader](https://github.com/TypeStrong/ts-loader)を導入すれば、webpack5はWebWorker用のTypeScriptファイルをトランスパイルし、エントリーファイルから読み込み可能な形で出力してくれます。

_▼tsconfig.json_

```json
{
  "compilerOptions": {
    "module": "ES2020",
    "target": "ES2015",
    "lib": ["es2020", "dom"],
  }
}
```

_▼webpack.config.js_

```js
module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
      },
    ],
  },
  resolve: {
    extensions: [
      '.ts', '.js',
    ],
  },
};
``` 

### TypeScriptのサンプル

_▼src/index.ts_

```ts
const worker = new Worker( new URL("./worker.ts", import.meta.url));

worker.postMessage("hello from main thread");
worker.onmessage = (e) => {
    console.log("on message in main thread : ", e.data);
    worker.terminate();
}
```

_▼src/worker.ts_

```ts
onmessage = (e) => {
    console.log( "on message in worker : ", e.data );
    postMessage( "hello from worker" );
}
```

ワーカーのグローバルスコープに`onmessage`関数を定義すると、別スレッドからのメッセージが受け取れます。処理済みのデータは`postMessage`関数でメインスレッドに返します。

メインスレッドから[`woker.terminate()`関数](https://developer.mozilla.org/ja/docs/Web/API/Worker/terminate)を呼び出すと、ワーカーは即座に終了します。

## 個人的な感想

webpack5のWebWorkerサポートが充実したことは大きな前進です。マルチスレッド処理によって処理時間が短縮できるケースは限られますが、ユーザー体験の向上につながる場面はありそうです。

以上、ありがとうございました。