【L1、L2】ブラウザでWeb Workerを動かしてみる（基本編）

## はじめに

2023年12月14日分のアドベントカレンダーの記事になります。

こんにちは、マーズフラッグの川嶋です。

今更で恐縮ではありますが、Web Workerを動かしてみようと思います。

Web WorkerはJSの処理をメインスレッドとは別のスレッドでコードを実行できます。
IOバウンドの問題を解決するのにはasync/awaitの利用して非同期処理で実現することで解決してきましたが、CPUバウンドの問題はWeb Workerを利用しマルチスレッド処理を実現することで対応できるとのこと。

弊社のフロントエンド開発でVue.jsを利用していおりますが、workerに処理を委譲したほうがよいようなシーンはまだありません。しかし、今後利用する機会もあるかもしれませんのでためしてみましょう！
最近はもっぱらバックエンドなんですけどね笑。

Mozillaのドキュメントに記載のある通りworkerには３種類あり、今回は一番確認しやすい専用ワーカー (dedicated worker) を取り上げようと思います。
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API

試してみるワーカーの種類は共有ワーカー (shared worker)でも利用方法はそれほど変わらないのでこちらを選択してのよかったのですが、残念ながらAndroid等の一部ブラウザではまだ対応していないようで、なるべくなら一番対応されているワーカーのほうがよいと思い選択しました。こちらは別の機会で取り上げようと思います。
https://developer.mozilla.org/ja/docs/Web/API/SharedWorker


## この記事で分かること

* ブラウザ環境で専用ワーカーを利用方法。
* ワーカーを確認環境としてtypescript+fastify+fastify/staticによる静的コンテンツ公開方法。
* 専用ワーカーを使いやすくするwokerpoolモジュールの利用方法。（こちら番外編として別記事にして記載します）

## 準備

では確認環境を作成していきましょう！
今回ブラウザにコンテンツを配信できるうようWebサーバーには、fastify＋typescriptを利用します。パッケージマネージャはpnpmを利用します。

以下のコマンドを実行してnodeプロジェクトを作成します。
```bash
mkdir sample-worker
cd ./sample-worker
npm init
省略…
```

必要なモジュールをインストールします。
```bash
pnpm install fastify @fastify/static
pnpm install -D typescript @types/node
pnpm install -D ts-node
```

Typescriptの設定の初期化と設定ファイルの作成します。
```bash
npx tsc --init
```
tsconfig.jsonの内容を以下に書き換えます。
```json-doc
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  }
}
```

packge.jsonに以下を追加します。
```json-doc
{
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "start": "node index.js",
    "dev": "node --no-warnings --loader ts-node/esm --watch 'src/server.ts'"
  },
  // 省略
  "type": "module",
  // 省略
}
```

ちなみにFastifyはどんなフレームワークか簡単に説明しますと（ChatGPTに聞いてみました）
>Fastifyは、Node.jsの高速で軽量なWebフレームワークで、主にRESTful APIやWebサービスを構築するために設計されています。高いパフォーマンス、軽量さ、スキーマベースのバリデーション、プラグインシステム、非同期対応が特徴です。

で、今回はFastifyで静的ファイルを利用しやすくする為に、Fastifyプラグインとして「@fastify/static」を利用します。
（前述のモジュールインストールでインストール済）

フォルダ構成を以下のようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/8d428019-fe57-e746-388e-b9739b256e66.png)

## 確認コードの作成

src/server.tsを以下内容で追加します。
```javascript
import fastify from "fastify";
import fastifyStatic from "@fastify/static"; // 静的ファイルを利用する為の設定
import path from "path";

const server = fastify();

// 静的ファイルを利用する為の設定
server.register(fastifyStatic, {
  root: path.join(process.cwd(), "public"),
});

server.get("/ping", async (request, reply) => {
  return "Hellow world\n";
});

server.listen({ port: 18080 }, (err, address) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log(`server is listening at ${address}`);
});
```

親画面、ワーカースクリプトを呼び出す画面
public/content/example.htmlを以下内容で追加します。
```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>ワーカーテスト１</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="data:," />
  </head>
  <body>
    <div>これはテストです。</div>
    <button id="btn1">ワーカーにデータを送信します。</button>
    <div>ワーカーから受信したデータは以下です。</div>
    <div id="received_data"></div>
  </body>
  <script>
    // Workerを読込む
    const myWorker1 = new Worker("/js/worker_script_1.js");

    // ボタンクリックしてWorkerにデータを送信する
    const btn1 = document.querySelector("#btn1");
    // Workerにデータを送信する
    btn1.addEventListener("click", () => {
      myWorker1.postMessage("worker_script_1.jsにデータ送信しました。");
    });

    // ワーカーからのデータを受信したデータを画面に表示する
    const divReceivedData = document.querySelector("#received_data");
    // ワーカーからのデータを受信する
    myWorker1.addEventListener("message", (e) => {
      console.log("worker_script_1.jsからデータを受信しました。:", e.data);
      divReceivedData.innerText = e.data;
    });
  </script>
</html>

```

ワーカースクリプトの作成
public/js/worker_script_1.jsを以下内容で追加します。
```javascript
let num = 0;
self.addEventListener(
  "message",
  (e) => {
    num += 1;
    console.log(`ワーカースクリプト内でコンソール出力：${e.data}`);
    self.postMessage(`${num}回目に受け取ったデータ => ${e.data}`);
  },
  false
);

```

作成したコードを含めたフォルダ構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/e940d046-e7a9-1497-d256-172063ff2db7.png)


## 実行及び結果

以下のコマンドで実行してみましょう。
>pnpm dev

上記コマンドの出力は以下のようになると思います。
```console
sample-worker$ pnpm dev

> sample-worker@1.0.0 dev /home/mfadmin/adv_cal/sample-worker
> node --no-warnings --loader ts-node/esm --watch 'src/server.ts'

server is listening at http://127.0.0.1:18080

```

ブラウザからアクセスしてみましょう！
以下のようなフローで画面が変化します。

* ボタン押すことで、ワーカーにデータが送信されます。
* 送信されたデータをワーカーが受信して、それをコンソールログに出力します。
* ワーカーは親画面にデータを返します。
* 親画面はワーカーから受信したデータをコンソールに出力します。
* 受信したデータを画面に表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/be489658-f0f3-4b8e-9262-eb1732f15a45.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/1482d6e7-ef75-b23e-7e16-a204dbf854da.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/201e230a-d025-eeaf-f8b4-2f1c8d93b648.png)

コンソールからワーカーを確認できます
以下のようにワーカーが動作しているがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/c34ffa3c-a7dd-401a-373c-108ff662ce52.png)