WebWorker ( Dedicated Worker ) を試す

WebWorker について調べる必要が出てきました。
外部のライブラリ(Chrome 関係から出てるので標準ではないけど、デファクトスタンダード?)もあるが、今回は標準 API だけ触っていきます。

参考

試す

Web Worker API には、Dedicated WorkerShared Worker があるということを今回調べていて知りました。
今回は、Dedicated Worker を試します。

環境準備

今回も環境の準備は、vite で準備します。

1
2
3
4
npm init @vitejs/app app --template vanilla
cd app
npm install
npm run dev

このタイミングで、main.js は main.ts に変更。
index.html での参照先も main.ts に変えておきます。

Worker の作成

main.ts で Worker として作成するソースコードを URL で指定します。

main.ts
1
2
3
4
5
6
const worker = new Worker("./worker.js");
console.log(worker);
// => Worker {onmessage: null, onerror: null}
// onerror: null
// onmessage: null
// __proto__: Worker

この時の worker.js は何も記述しなくても Worker は作成できる。

main スレッド(UI スレッド) <= Worker スレッド 方向の通信

続けて、main スレッド(UI スレッド) <= Worker スレッド 方向の通信を試します。

main.ts
1
2
3
4
5
6
const worker = new Worker("worker.ts");

worker.onmessage = (event: MessageEvent) => {
console.log(event);
console.log(event.data);
};
worker.ts
1
2
3
4
5
6
const _worker: Worker = self as any;

setInterval(() => {
// Mainスレッドへ送信
_worker.postMessage(new Date().toLocaleTimeString());
}, 1000);

これを動かすとデベロッパーツールのコンソールでは次のように表示されます。

1
2
3
4
5
6
7
8
9
10
11
MessageEvent {isTrusted: true, data: "15:45:52", origin: "", lastEventId: "", source: null, …}
bubbles: false
cancelBubble: falsecancelable: false
composed: false
....

15:45:52

MessageEvent {isTrusted: true, data: "15:45:53", origin: "", lastEventId: "", source: null, …}

15:45:53

Worker スレッドでpostMessage()を呼び出した結果、main スレッドの、onmessageイベントに定義したメソッドのイベントで受け取りましした。
main スレッド(UI スレッド) <= Worker スレッド 方向のスレッド間通信できました。

main スレッド(UI スレッド) => Worker スレッド方向の通信

main スレッド から、Worker スレッドに通信して、計算結果を受け取ってみます。

main.ts
1
2
3
4
5
6
7
8
9
const worker = new Worker("worker.ts");

// Workerスレッドから受信
worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

// Workerスレッドへ送信
worker.postMessage([1, 2, 3]);
worker.ts
1
2
3
4
5
6
7
8
const _worker: Worker = self as any;

_worker.onmessage = (event: MessageEvent) => {
const [a, b, c] = event.data;

// Mainスレッドへ送信
_worker.postMessage(a + b + c);
};

実行すると、main スレッド側で 6 を受け取って結果を表示できました。
off-the-main-thread という考え方があって、「main スレッドを 16ms 以上止めてはダメ」という考え方があるそうです。
main スレッドの処理を別スレッドに移譲することいい、main スレッドを占有するとユーザー操作を阻害してしまうからだそうです。

処理負荷の高い処理であれば、Worker スレッドに以上するというのが大事なんですね。

WebWorker のエラーを拾う

Worker スレッドのエラーを Main スレッドで拾うことができます。

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const worker = new Worker("worker.ts");

// Workerスレッドから受信
worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

// Workerスレッドのエラーをキャッチ
worker.onerror = (event: ErrorEvent) => {
console.log(event);
console.log(event.message);
};

// Workerスレッドへ送信
worker.postMessage([1, 2, 3]);
worker.ts
1
2
3
4
5
6
7
8
9
10
11
const _worker: Worker = self as any;

_worker.onmessage = (event: MessageEvent) => {
const [a, b, c] = event.data;

// わざと d の未定義エラーを起こす
const e = d;

// Mainスレッドへ送信
_worker.postMessage(a + b + c);
};

実行すると次のように、
これを動かすとデベロッパーツールのコンソールでは次のように表示されます。

1
2
3
ErrorEvent {isTrusted: true, message: "Uncaught ReferenceError: d is not defined", filename: "http://localhost:8080/worker.ts", lineno: 4, colno: 13, …}

Uncaught ReferenceError: d is not defined

エラーを拾うことができました。

Worker スレッドの終了

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const worker = new Worker("worker.ts");

// Workerスレッドから受信
worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

// Workerスレッドのエラーをキャッチ
worker.onerror = (event: ErrorEvent) => {
console.log(event);
console.log(event.message);
};

// workerスレッドを終了
worker.terminate();

// Workerスレッドへ送信しても終了しているので、応答しない
worker.postMessage([1, 2, 3]);
worker.ts
1
2
3
4
5
6
7
8
9
10
11
const _worker: Worker = self as any;

_worker.onmessage = (event: MessageEvent) => {
const [a, b, c] = event.data;

// わざと d の未定義エラーを起こす
const e = d;

// Mainスレッドへ送信
_worker.postMessage(a + b + c);
};

terminate() を実行することで、Worker が終了します。
終了後に postMessage() を実行しても特にエラー起こらず反応はありません。

できないこと

worker スレッドでは DOM に触ることができません。
例えば以下は無理です。

worker.ts
1
document.getElementById("app").innerText = "from worker";

実行するとデベロッパーコンソールでは、 Uncaught ReferenceError: document is not defined とエラーになります。

なら document を渡せばイイか?と考えて以下を試します。

main.ts
1
2
3
4
5
6
7
8
9
const worker = new Worker("worker.ts");

// Workerスレッドから受信
worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

// Workerスレッドへドキュメントを渡す
worker.postMessage([document]);
worker.ts
1
2
3
4
5
6
7
const _worker: Worker = self as any;

_worker.onmessage = (event: MessageEvent) => {
const [document] = event.data;

document.getElementById("app").innerText = "from worker";
};

実行すると、デベロッパーコンソールでは次のエラーになります。

1
Uncaught DOMException: Failed to execute 'postMessage' on 'Worker': HTMLDocument object could not be cloned.

HTMLDocument オブジェクトは渡せない(クローンできない)んですね。

Worker でスクリプトを読み込む

Worker スレッドで、importScripts()を使うことで他のスクリプトを読むことができます。
注意するポイントは、「グローバル関数やスクリプト」を読み込む機能なので、export が使えません。

以下のように使います。

func.ts
1
2
3
4
5
6
7
const func = (params: [number]) => {
let tmp = 0;
params.forEach((p) => {
tmp += p;
});
return tmp;
};
worker.ts
1
2
3
4
5
6
7
8
9
10
11
// 外部スクリプトの読み込み
importScripts("func.ts");

const _worker: Worker = self as any;

_worker.onmessage = (event: MessageEvent) => {
const tmp = func(event.data);

// Mainスレッドへ送信
_worker.postMessage(tmp);
};
main.ts
1
2
3
4
5
6
7
8
9
const worker = new Worker("worker.ts");

// Workerスレッドから受信
worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

// Workerスレッドへ送信
worker.postMessage([1, 2, 3, 4]);

実行すると、デベロッパーコンソールで 10 を返してくれます。

Worker でスクリプトを読み込む 2

次のようにダイレクトインポートを使えば、Worker スレッドで読み込む外部スクリプトでも export が使えます。

func.ts
1
2
3
4
5
6
7
export const func = (params: [number]) => {
let tmp = 0;
params.forEach((p) => {
tmp += p;
});
return tmp;
};
worker.ts
1
2
3
4
5
6
7
8
9
10
const _worker: Worker = self as any;

_worker.onmessage = async (event: MessageEvent) => {
// 外部スクリプトのダイレクトインポート
const { func } = await import("./func");
const tmp = func(event.data);

// Mainスレッドへ送信
_worker.postMessage(tmp);
};

main.ts はそのまま。
実行すると、こちらでも 10 を返してくれました。

Vite で WebWorker

ここで、vite でビルドをすると 1 つのことに気が付きます。
main.ts しかビルドされていないじゃないですか!
ということで、調べてみるとワーカースクリプト(Worker として動かすスクリプト)は、別の読み込み方法が必要でした。

main.ts
1
2
3
4
5
6
7
8
9
10
11
// ?worker をつけて import
import MyWorker from "./worker?worker";

// import したものから直接 Worker を作成
const worker = new MyWorker();

worker.onmessage = (event: MessageEvent) => {
console.log(event.data);
};

worker.postMessage([1, 2, 3, 4]);

?worker をつけることで、worker.ts もビルドの対象になります。
ビルドされた、.js ファイルを見ると、worker.ts func.ts のそれぞれが、トランスパイルのがわかります。

'./worker?worker&inline' としてあげると、インポート対象が以下のように、Base64 エンコードされた状態になります。

index.cdcfcff7.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const b = new Blob(
[
atob(
"bGV0IGU7Y29uc3QgdD17fSxuPXNlbGY7bi5vbm1lc3NhZ2U9YXN5bmMgcj0+e2NvbnN0e2Z1bmM6c309YXdhaXQgZnVuY3Rpb24obixyKXtpZighcilyZXR1cm4gbigpO2lmKHZvaWQgMD09PWUpe2NvbnN0IHQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgibGluayIpLnJlbExpc3Q7ZT10JiZ0LnN1cHBvcnRzJiZ0LnN1cHBvcnRzKCJtb2R1bGVwcmVsb2FkIik/Im1vZHVsZXByZWxvYWQiOiJwcmVsb2FkIn1yZXR1cm4gUHJvbWlzZS5hbGwoci5tYXAoKG49PntpZihuIGluIHQpcmV0dXJuO3Rbbl09ITA7Y29uc3Qgcj1uLmVuZHNXaXRoKCIuY3NzIikscz1yPydbcmVsPSJzdHlsZXNoZWV0Il0nOiIiO2lmKGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoYGxpbmtbaHJlZj0iJHtufSJdJHtzfWApKXJldHVybjtjb25zdCBvPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImxpbmsiKTtyZXR1cm4gby5yZWw9cj8ic3R5bGVzaGVldCI6ZSxyfHwoby5hcz0ic2NyaXB0IixvLmNyb3NzT3JpZ2luPSIiKSxvLmhyZWY9bixkb2N1bWVudC5oZWFkLmFwcGVuZENoaWxkKG8pLHI/bmV3IFByb21pc2UoKChlLHQpPT57by5hZGRFdmVudExpc3RlbmVyKCJsb2FkIixlKSxvLmFkZEV2ZW50TGlzdGVuZXIoImVycm9yIix0KX0pKTp2b2lkIDB9KSkpLnRoZW4oKCgpPT5uKCkpKX0oKCgpPT5pbXBvcnQoIi4vZnVuYy1kY2ZlYjAzMC5qcyIpKSx2b2lkIDApLG89cyhyLmRhdGEpO24ucG9zdE1lc3NhZ2Uobyl9Owo="
),
],
{ type: "text/javascript;charset=utf-8" }
);
const c = new (function () {
const c = (window.URL || window.webkitURL).createObjectURL(b);
try {
return new Worker(c);
} finally {
(window.URL || window.webkitURL).revokeObjectURL(c);
}
})();
(c.onmessage = (b) => {
console.log(b.data);
}),
c.postMessage([1, 2, 3, 4]);

vite がすごいですね。ありがたい。
worker のビルドって、別にするのかな?とも考えていたのですが、考慮済みでした。
webpack はどうなのか?とみてみると、こちらも worker-loader が用意されているので、こちらを使えばいいようです。
杞憂でしたね。

今後調べること

WebWorker 関連のワードとして以下のようなものがあるので、順繰りに試していきたいと考えています。

  • Shared Worker
  • comlink
  • clooney
  • OffscreenCanvas
  • worker-dom

今回は WebWorker(Dedicated Worker) を触ってみました。
WebWorker といいつつ、最近だと Node.js や Deno でも呼び出せるので、そちらでも生かしたいところです。

ではでは。