はじめに
Web Workerを使って重い処理をメインスレッドから切り離したい。そう思って実装を始めた瞬間、多くの人がpostMessageとonmessageの煩雑さにつまずきます。関数を呼びたいだけなのに、メッセージの種類を判定するswitch文を書き、リクエストIDを管理して、レスポンスを紐付ける……。この定型作業が積み重なると、本来書きたかったロジックがどこにあるのか分からなくなってしまいます。
Comlinkは、この「postMessageについて考える精神的障壁」を取り除いてくれるライブラリです。Worker側に公開したオブジェクトを、メインスレッド側からまるでローカルにあるかのように呼び出せるようにしてくれます。
Comlinkとは
ComlinkはGoogle Chromeチームが開発している軽量なRPC(リモートプロシージャコール)ライブラリです。ES6のProxyを活用し、Web Workerとメインスレッドの間でオブジェクトや関数をやり取りする処理を透過的にラップします。
主な特徴
- 驚異的な軽さ - gzip圧縮で約1.2KB、brotli圧縮では約1.1KBしかなく、導入コストがほぼありません
- 透過的なプロキシ -
wrap()とexpose()だけで、関数呼び出し・プロパティアクセス・オブジェクト生成までWorker越しに扱えます - 広いブラウザ対応 - Chrome 56+、Firefox 52+、Safari 10.1+、Edge 15+など、モダンブラウザを幅広くカバーします
- Transferableのサポート -
transfer()を使えば、ArrayBufferなどの所有権をコピーなしで移動でき、大きなデータのやり取りも高速です
インストール
npmから通常のパッケージとしてインストールできます。
npm install comlink
# または
yarn add comlink
# または
pnpm add comlink
CDNから直接読み込むことも可能です。
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
基本的な使い方
まず、Worker側で公開したい処理をオブジェクトとしてまとめ、Comlink.expose()で公開します。
// worker.js
const api = {
counter: 0,
increment() {
this.counter++;
return this.counter;
},
async heavyCalculation(n) {
// 重い計算処理を想定
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
},
};
Comlink.expose(api);
メインスレッド側では、Comlink.wrap()でWorkerをラップするだけで、公開されたAPIをそのまま呼び出せます。
// main.js
import * as Comlink from "comlink";
const worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
const api = Comlink.wrap(worker);
async function main() {
await api.increment();
const count = await api.counter;
console.log(`Counter: ${count}`);
const result = await api.heavyCalculation(1_000_000);
console.log(`Result: ${result}`);
}
main();
プロパティアクセスも関数呼び出しも、Worker越しであることを意識せずにawaitを付けるだけで完結する点がComlinkの大きな魅力です。
実践的なユースケース
大きな配列データをWorkerに渡して集計させるケースを考えてみます。通常であればデータのコピーコストが発生しますが、Comlink.transfer()を使えばTransferableオブジェクトの所有権をそのまま移動できます。
// worker.js
const api = {
sumFloat64Array(buffer) {
const view = new Float64Array(buffer);
let sum = 0;
for (const value of view) {
sum += value;
}
return sum;
},
};
Comlink.expose(api);
// main.js
import * as Comlink from "comlink";
const worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
const api = Comlink.wrap(worker);
async function calculateSum(data) {
const buffer = data.buffer;
// 第2引数にTransferableな配列を渡すと、コピーなしで所有権が移動する
const sum = await api.sumFloat64Array(Comlink.transfer(buffer, [buffer]));
console.log(`Sum: ${sum}`);
}
const data = new Float64Array(1_000_000).map(() => Math.random());
calculateSum(data);
このように、画像処理・大規模データの集計・機械学習の推論処理など、メインスレッドをブロックしたくない重い処理をWorkerに逃がしつつ、コードの見た目はほぼ同期処理のまま保てるのがComlinkの強みです。UIの応答性を落とさずに計算処理を並列化したい場面で、特に効果を発揮します。
まとめ
Comlinkは、Web Workerとのやり取りにつきまとうpostMessageの定型処理を、Proxyベースの透過的なAPIに置き換えてくれるライブラリです。1KB強という軽さでありながら、関数呼び出し・プロパティアクセス・Transferableの転送までカバーしており、「重い処理をWorkerに切り出したいけれど実装が面倒」という課題を素直に解決してくれます。
まだpostMessageを手書きしている処理があれば、まずは小さな関数ひとつからComlinkに置き換えてみてはいかがでしょうか。
