Web Workerとは、一部のスクリプトをメインスレッドから別のスレッドに分離し、UIの描画を阻害せずに処理を行うことができる仕組みです。
JavaScriptはかつて完全にシングルスレッドの言語で、UIを描画するメインスレッドで全ての処理を行っていました。計算負荷の高い処理を行うとページがフリーズしてしまうため、重い処理はできないのが常識でした。
Web Workerを使用すれば、非同期処理では対応できない重い処理も並列化して別スレッドで実行できます。
JavaScriptのコールバック関数と非同期処理を丁寧に理解する本記事では触って確かめられる実例も交えて、Web Workerの基本の使い方を紹介します。
Web Worker APIはシンプルなAPIで、メインスレッドで動作するスクリプトと、ワーカースレッドで動作するスクリプトの間でメッセージングを行い使用します。
まず、以下のようにJavaScriptファイルと、HTMLファイルを用意しましょう。
.
└── index.html/
└── js/
├── main.js
└── worker.js
index.html
:ワーカーを使用するページのHTMLjs
:JSファイルを入れておくディレクトリmain.js
:メインスレッドで動作する通常のJSworker.js
:Web Worker用のJS
最初に、メインスレッドで動作するmain.jsで、Web Workerを使用することを宣言します。
‘ . / js / worker.js ‘:Worker()に指定するファイルの場所は、HTMLファイルからみた位置(パス)で指定します。
次に、main.js
からworker.js
にメッセージを送り、同時にworker.js
からのレスポンス(返答)を待機します。
以下はメインスレッドからワーカースレッドに挨拶(’greeting’
)を要求する、Web Workerの最も簡単な例です。
メインスレッドとワーカースレッドでやり取りするメッセージはJavaScriptのオブジェクトで作り、postMessage()
で送ります。
好きなプロパティをいくつでも設定できますが、ここでは、command
プロパティにメッセージの種類('greeting'
)を設定しています。
メインスレッド側では(ワーカーからいつレスポンスが来ても良いように)イベントリスナーで事前に待機処理を記述しておかなければなりません。
レスポンスの待機で、switch ... case
文を用いているのは、複数のやり取り(メッセージング)を追加した際に、ワーカーからのレスポンスの種類を正しく認識するためです。
- JSオブジェクトを
postMessage()
で送る - メッセージの中身は
e.data
から取り出す - イベントリスナーで、レスポンスに対する待機処理を事前に記述する
次に、ワーカースレッドで動作するworker.js
を書いていきます。
イベントリスナーでmain.js
からのメッセージに待機します。
基本の処理はmain.js
とほぼ同じです。main.js
からメッセージが受け取った時にpostMessage()
でレスポンスしている部分だけ注意しましょう。
ここでは、command
オプションが'greeting'
のメッセージだったら、レスポンスとして、"Hello, Main Thread. I'm a Web Worker."
と挨拶を返すようにしています。
以上で、Web Workerによる並列化処理は完了です。
本当にわずかな時間ですが、メインスレッドからメッセージが送られてからワーカーが挨拶を返すまで、そのやり取りに要する内部処理はメインスレッドから分離されています。
実用上は、Web Workerで分離したいのは挨拶ではなく「重い処理」です。次章では実際にその違いを確認しましょう。
本章では、Web Workerを使用した時と使用しなかった時の違いを実例で確認してみましょう。
「重い処理」をメインスレッドで無理やり実行した時と、Web Workerに分離した場合の違いが分かるページを作りました。
「重い処理」は次節に記載しますが、誤解を恐れずざっくり言うと、決められたコストの中で最も良い買い物をするような計算をしています。
フォームに2桁か3桁の数字(コスト)を入力して、「In UI Thread」のボタンを押してみてください。すると、計算中は明らかにページがフリーズし、ストップウォッチが停止してしまいます。
一方で、「In Web Worker」のボタンを押してみてください。同じ処理をしていてもワーカースレッドで動くため、アニメーションが止まらないことが分かりますね。
デモページのソースコードはGitHubにあります。
余談ですが、デモページの「重い処理」がなぜ重いのか記載しておきます。現実世界で、同じ計算を実装することは無いので、詳細が気にならない人は飛ばしてください。
買い物の計算という例え話を正確に言い直すと、実際に裏側で動いている処理は、「最適化されていないナップサック問題」です。
ナップサック問題とは、以下のような有名問題です。
N個の商品があり、各商品にはそれぞれコストwと価値vが割り当てられている。
選んだ商品のコストの合計total(w)が制約Wを超えないような選び方のうち、選んだ商品の価値total(v)が最大となる選び方を解答してください。
デモページの「FULL GOODS LIST」ボタンを押すと、商品の一覧が見られます。その例で説明すると、
- 制約Wは、フォームに入力した「ショッピングカートの大きさ」
- コストwは、各商品のSize
- 価値vは、各商品のValue
となります。
「商品はいくつ選んでもいいが、価値の合計total(v)を最大にしなければならない」というところがポイントで、仮に愚直に計算しようとすると、
- 商品1を選ぶ or 選ばない → 2通り
- 商品2を選ぶ or 選ばない → 2通り
- …
→ 2N通り
を全て調べないと解答できません。2N通りは膨大な数で、商品が10個でも1024通りあり、デモページの例だと3300万通り程度あります。
選び方全てに対して、1つ1つコストの合計、価値の合計を計算しているので、「重い処理」となるわけですね。
動的計画法(DP):
実はナップサック問題には計算量を大幅に削減できる動的計画法が知られています。アルゴリズムを解説する記事では無いので省きますが
実用上JavaScriptでWeb Workerが使用される例は主に以下の3つでしょう。
- グラフィック処理(リッチなWeb表現)
- データやモデル等、重いファイルの読み込み
- 外部APIへの接続
グラフィック処理は、幾何学模様や3DモデルなどのリッチなWeb表現を描画する処理です。
2番目に関しては、特に昨今重要なのがAIモデルのローディングです。AIモデルはサイズが大きい傾向にあるので、ローディングはネックになりやすいところです。
3番目は、Webアプリやサイトだけでなく外部の環境に依存するため、時間がかかるかかからないかを事前に予測するのが難しい例ですね。時間がかかった時にメインスレッドを阻害したくない場合に利用します。
便利なWeb Workerですが、知っておかなければならない制約が2点あります。
- ワーカースレッドからは、DOM要素を操作できない
- 同じ変数への同時アクセスは不可
JSの役割は急速に拡大していますが、今も昔もDOM要素の管理が最も重要な役割の1つであることに変わりはありません。
そのため、DOM要素の操作はメインスレッドのみに許可されており、ワーカースレッドに委任することはできません。
例えば、以下のように、メインスレッドで何か動かしている間に、ワーカースレッドでボタンの色を変更することは出来ません。
一見不便ですが、仮にDOM要素の操作を並列化できた場合、メインスレッドとワーカースレッドで同時に同じボタンの色を変更することもできてしまいそうです。
すると、時にはボタン色が赤で、別の時にはボタン色が緑、みたいなことが起こってしまう恐れがあり危険です。JSの性質を考えると、実はもっともな制約です。
Web Workerは特定の処理をメインスレッドから分離しますが、メインスレッドとワーカースレッドの同じ変数への同時アクセスは不可になっています。
例えば、100個の数字が1つの配列に格納されていて、100個の数字の合計を求めたいとします。
メインスレッドとワーカースレッドで同時に同じ配列にアクセスして、メインスレッドでは前半の50個、ワーカースレッドで後半の50個を同時に足し算して、後で結果を集計する、と言う処理はできません。
正確には、同時に計算できないわけではなく、後半の50個のデータのコピーを作成して、メインスレッドからワーカースレッドに渡す必要があります。
並列化を知っている方なら指摘したくなる部分でしょうが、Web Workerによる並列化は、マルチスレッドというよりむしろマルチプロセス化と言った方が正しいのかも知れません。
以上でWeb Workerの基本の使い方と知識についての説明を終わります。
最後に、ポイントをまとめておきますので参照してください。お疲れ様でした。
- メインスレッド用のJSとワーカースレッド用のJ’Sを用意する
- メッセージはJSオブジェクトで送り、イベントリスナでレスポンスを待機する
- 実用上は、グラフィックスやAIモデルのロードなどに使われる
- JSの役割や仕様に基づく並列化の制約に注意する