WebWorker について調べる必要が出てきました。
外部のライブラリ(Chrome 関係から出てるので標準ではないけど、デファクトスタンダード?)もあるが、今回は標準 API だけ触っていきます。
参考
- mdn - Web Worker の使用
- Off-the-main-thread with WorkerDom
- nhiroki’s weblog - ウェブブラウザの off-the-main-thread API の話
- ics.media - オフスクリーンキャンバスを使ったJavaScriptのマルチスレッド描画 - スムーズなユーザー操作実現の切り札
- mizchi’s blog - off-the-main-thread の時代
- mizchi’s blog - この DOM がすごい2018: worker-dom
- MDN - WorkerGlobalScope.importScripts()
- Vite - Web Workers
- webpack - worker-loader
試す
Web Worker API には、Dedicated Worker
と Shared Worker
があるということを今回調べていて知りました。
今回は、Dedicated Worker
を試します。
環境準備
今回も環境の準備は、vite で準備します。
1 | npm init @vitejs/app app --template vanilla |
このタイミングで、main.js は main.ts に変更。
index.html での参照先も main.ts に変えておきます。
Worker の作成
main.ts で Worker として作成するソースコードを URL で指定します。
1 | const worker = new Worker("./worker.js"); |
この時の worker.js は何も記述しなくても Worker は作成できる。
main スレッド(UI スレッド) <= Worker スレッド 方向の通信
続けて、main スレッド(UI スレッド) <= Worker スレッド 方向の通信を試します。
1 | const worker = new Worker("worker.ts"); |
1 | const _worker: Worker = self as any; |
これを動かすとデベロッパーツールのコンソールでは次のように表示されます。
1 | MessageEvent {isTrusted: true, data: "15:45:52", origin: "", lastEventId: "", source: null, …} |
Worker スレッドでpostMessage()
を呼び出した結果、main スレッドの、onmessage
イベントに定義したメソッドのイベントで受け取りましした。
main スレッド(UI スレッド) <= Worker スレッド 方向のスレッド間通信できました。
main スレッド(UI スレッド) => Worker スレッド方向の通信
main スレッド から、Worker スレッドに通信して、計算結果を受け取ってみます。
1 | const worker = new Worker("worker.ts"); |
1 | const _worker: Worker = self as any; |
実行すると、main スレッド側で 6 を受け取って結果を表示できました。off-the-main-thread
という考え方があって、「main スレッドを 16ms 以上止めてはダメ」という考え方があるそうです。
main スレッドの処理を別スレッドに移譲することいい、main スレッドを占有するとユーザー操作を阻害してしまうからだそうです。
処理負荷の高い処理であれば、Worker スレッドに以上するというのが大事なんですね。
WebWorker のエラーを拾う
Worker スレッドのエラーを Main スレッドで拾うことができます。
1 | const worker = new Worker("worker.ts"); |
1 | const _worker: Worker = self as any; |
実行すると次のように、
これを動かすとデベロッパーツールのコンソールでは次のように表示されます。
1 | ErrorEvent {isTrusted: true, message: "Uncaught ReferenceError: d is not defined", filename: "http://localhost:8080/worker.ts", lineno: 4, colno: 13, …} |
エラーを拾うことができました。
Worker スレッドの終了
1 | const worker = new Worker("worker.ts"); |
1 | const _worker: Worker = self as any; |
terminate()
を実行することで、Worker が終了します。
終了後に postMessage()
を実行しても特にエラー起こらず反応はありません。
できないこと
worker スレッドでは DOM に触ることができません。
例えば以下は無理です。
1 | document.getElementById("app").innerText = "from worker"; |
実行するとデベロッパーコンソールでは、 Uncaught ReferenceError: document is not defined
とエラーになります。
なら document
を渡せばイイか?と考えて以下を試します。
1 | const worker = new Worker("worker.ts"); |
1 | const _worker: Worker = self as any; |
実行すると、デベロッパーコンソールでは次のエラーになります。
1 | Uncaught DOMException: Failed to execute 'postMessage' on 'Worker': HTMLDocument object could not be cloned. |
HTMLDocument オブジェクトは渡せない(クローンできない)んですね。
Worker でスクリプトを読み込む
Worker スレッドで、importScripts()
を使うことで他のスクリプトを読むことができます。
注意するポイントは、「グローバル関数やスクリプト」を読み込む機能なので、export
が使えません。
以下のように使います。
1 | const func = (params: [number]) => { |
1 | // 外部スクリプトの読み込み |
1 | const worker = new Worker("worker.ts"); |
実行すると、デベロッパーコンソールで 10 を返してくれます。
Worker でスクリプトを読み込む 2
次のようにダイレクトインポートを使えば、Worker スレッドで読み込む外部スクリプトでも export
が使えます。
1 | export const func = (params: [number]) => { |
1 | const _worker: Worker = self as any; |
main.ts はそのまま。
実行すると、こちらでも 10 を返してくれました。
Vite で WebWorker
ここで、vite でビルドをすると 1 つのことに気が付きます。
main.ts しかビルドされていないじゃないですか!
ということで、調べてみるとワーカースクリプト(Worker として動かすスクリプト)は、別の読み込み方法が必要でした。
1 | // ?worker をつけて import |
?worker
をつけることで、worker.ts もビルドの対象になります。
ビルドされた、.js
ファイルを見ると、worker.ts func.ts のそれぞれが、トランスパイルのがわかります。
'./worker?worker&inline'
としてあげると、インポート対象が以下のように、Base64 エンコードされた状態になります。
1 | const b = new Blob( |
vite がすごいですね。ありがたい。
worker のビルドって、別にするのかな?とも考えていたのですが、考慮済みでした。
webpack はどうなのか?とみてみると、こちらも worker-loader
が用意されているので、こちらを使えばいいようです。
杞憂でしたね。
今後調べること
WebWorker 関連のワードとして以下のようなものがあるので、順繰りに試していきたいと考えています。
- Shared Worker
- comlink
- clooney
- OffscreenCanvas
- worker-dom
今回は WebWorker(Dedicated Worker) を触ってみました。
WebWorker といいつつ、最近だと Node.js や Deno でも呼び出せるので、そちらでも生かしたいところです。
ではでは。