【JavaScript】WebWorkerのメッセージングにBroadcastChannelを使うメリットとデメリット

こんにちわ。

今回は、みんな大好きWebWorkerのお話です。
新しいインターフェイスとその使いかたを知ったので、まとめてみようと思います。

# 普通のWebWorker
WebWorkerを使うとき、普通は`Worker`に備え付けられたメッセージングインターフェイスでスレッド間の通信を行うと思います。

```js:メインスレッド
const worker = new Worker("./worker.js");
worker.addEventListner("message", ({data})=>{});
worker.postMessage("ほげほげ", []);
```
```js:ワーカースレッド
// self => DedicatedWorkerGlobalScope
self.addEventListener("message", ({data})=>{
    self.postMessage("ほげほげ");
});
```

普通に使うなら、これで充分な場合がほとんどです。
特に`DedicatedWorker`の場合、そのコンテキスト内で処理を完結させるケースがほとんどなので、困ることはないと思います。

しかし、例えばバックエンドサーバーとクライアントアプリケーションの間でAPIの仲介を行う、半永続的なワーカースレッドを生やすとします。

この場合、アプリケーションのあらゆるコンテキストからワーカースレッドへアクセスされることが予想されます。

ワーカースレッドを複数のコンテキストで共有したい場合、まず`SharedWorker`が浮かぶと思います。

しかしこの`SharedWorker`は、コンテキストを跨いだメッセージングを行うために`DedicatedWorker`では隠蔽されていた`MessagePort`というメッセージングインターフェイスを操作する必要があり、これが少々厄介です。

```js:メインスレッド
const worker = new SharedWorker("./worker.js");
worker.port.start();
worker.port.addEventListener("message", ({data})=>{});
worker.port.postMessage("ほげほげ", []);
```
```js:ワーカースレッド
// self => SharedWorkerGlobalScope
self.addEventListener("connect", ({ports})=>{
    ports[0].addEventListener("message", ({data})=>{
        ports[0].postMessage("ほげほげ", []);
    });
    ports[0].start();
});
```

\* 厳密には`DedicatedWorker`のメッセージングインターフェイスにも`MessagePort`は存在しますが、コンテキストはひとつなのでユーザーからは隠蔽され暗示的に操作されます。

# BroadcastChannel
- [BroadcastChannel - MDN](https://developer.mozilla.org/ja/docs/Web/API/BroadcastChannel)
- [Channel Messaging API - MDN](https://developer.mozilla.org/ja/docs/Web/API/Channel_Messaging_API)

`BroadcastChannel`は、コンテキストを跨いだメッセージングを簡単に行うためのインターフェイスです。

コンストラクタでチャネルを指定でき、そのチャネルが存在しない場合は作成し、存在する場合は参加します。
作成もしくは参加した後は、ブロードキャストと名にある通りそのチャネルへ参加しているコンテキストへメッセージを配信できます。

メインスレッド内同士はもちろんワーカースレッドやiframeなども横断してアクセスできます。

`BroadcastChannel`も他のメッセージングインターフェイスと同様にChannel Messaging APIがベースとなっているため、実際のコーディングも似たような感じとなっています。

`BroadcastChannel.prototype.postMessage()`で、そのチャネルへ接続しているすべてのコンテキストへ向けてメッセージを配信します。
`BroadcastChannel.prototype.onmessage`で、配信されたすべてのメッセージを購読します。
`BroadcastChannel.prototype.close()`で、チャネルへの接続を終了し、コンテキストをガベージコレクタに回収してもらえるようにします。

```js:複数のコンテキスト
// "my-channel"は存在しないため作成される
const ctx1 = new BroadcastChannel("my-channel");
ctx1.addEventListner("message", ({data})=>{
    ctx1.close();
});
ctx1.postMessage("ほげほげ");

// "my-channel"は存在するため参加される
const ctx2 = new BroadcastChannel("my-channel");
ctx2.addEventListner("message", ({data})=>{
    ctx2.close();
});
ctx2.postMessage("ほげほげ");
```

# Worker × BroadcastChannel
WebWorkerでスレッド間通信を行う場合、備え付けのメッセージングインターフェイスを使用するのではなく、代わりに`BroadcastChannel`を使うことができます。

```js:メインスレッド
new Worker("./worker.js");

const ctx1 = new BroadcastChannel("worker-channel");
ctx1.addEventListener("message", ({data})=>{
    ctx1.close();
});
ctx1.postMessage("ほげほげ");

const ctx2 = new BroadcastChannel("worker-channel");
ctx2.addEventListener("message", ({data})=>{
    ctx2.close();
});
ctx2.postMessage("ほげほげ");
```
```js:ワーカースレッド
const bc = new BroadcastChannel("worker-channel");
bc.addEventListener("message", ({data})=>{
    bc.postMessage("ほげほげ");
});
```
本来であれば`DedicatedWorker`は複数のコンテキストを跨ぐことはできませんが`BroadcastChannel`と組み合わせることで`MessagePort`を隠蔽しつつコンテキストを跨ぐことができます。

なお`Worker`コンストラクタは、宣言した時点でスクリプト解析とスレッド生成と実行が開始されるため、ワーカースレッドを強制停止(`Worker.prototype.terminate()`)させる必要がなければ、インスタンス変数を充てがう必要はありません。

## メリット
まず`SharedWorker`と違い`MessagePort`を操作する必要がないため、複数のコンテキストが存在していたとしてもメッセージングを簡単に実装できます。

また、ワーカースレッドとメッセージングのオブジェクトを分離することにより、コードの見通しが良くなるという利点も存在します。

ひとつのワーカースレッドに複数のチャネルを設けて集中処理こさせることも可能ですし、メッセージを受け取って処理するスレッドをワーカースレッドからメインスレッドへ移す場合も`BroadcastChannel`のブロックを丸ごと移動させるだけで済みます。

特に最近のコンポーネント志向なフロントエンドフレームワークにおいては、強力な武器となるでしょう。

## デメリット
`BroadcastChannel`は比較的ローレベルでシンプルなメッセージングインターフェイスのため、やりとりするメッセージに識別子を埋め込むなどして、目的のメッセージのみを購読するためのロジックを用意する必要があります。

また、最大のデメリットは`BroadcastChannel`には`Transferable`インターフェイスが存在しません。
やりとりされるメッセージは全てコピーされます。
つまり、巨大なバイナリバッファのやりとりは不向きとなります。