Web Worker で重い処理を裏で回す

Recipe ID: perf-008

UI スレッド(メインスレッド)で重い計算を行うと、ボタンが反応しなくなったりアニメーションが止まったりします。
Web Worker を使用して別スレッドで処理を実行し、UI のレスポンスを維持します。
Vite では特別な設定なしに Worker をインポートできます。

1. Worker ファイルの作成

src/workers/heavy-calculation.ts:

// 自己完結したコンテキストで動作するコード
self.onmessage = (e: MessageEvent<number>) => {
  const num = e.data;
  const result = fibonacci(num);
  self.postMessage(result);
};

function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

2. メインスレッドからの呼び出し

メインスレッド(UI スレッド)から呼び出します。
Vite では new Worker コンストラクタと new URL を組み合わせて Worker を生成するのが標準的です。

const resultEl = document.getElementById('result');
const buttonEl = document.getElementById('btn-calc');

if (buttonEl) {
  buttonEl.addEventListener('click', () => {
    // Worker インスタンス生成
    const worker = new Worker(new URL('./workers/heavy-calculation.ts', import.meta.url), {
      type: 'module',
    });

    worker.onmessage = (e: MessageEvent<number>) => {
      if (resultEl) {
        resultEl.textContent = `Result: ${e.data}`;
      }
      worker.terminate(); // 使い終わったら破棄
    };

    worker.postMessage(40); // 計算開始
  });
}

3. Comlink の活用

postMessage によるメッセージングは低レベルで扱いづらいため、Google 製のライブラリ comlink を使うと、Worker 内の関数を非同期関数として透過的に呼び出せるようになります。

npm install comlink

これにより、RPC のように直感的に記述できます。

src/workers/heavy-comlink.ts:

import * as Comlink from 'comlink';

function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const api = {
  fibonacci,
};

// 型をエクスポートしておくとメインスレッド側で便利です
export type HeavyApi = typeof api;

Comlink.expose(api);

メインスレッド側:

import * as Comlink from 'comlink';
import type { HeavyApi } from '../workers/heavy-comlink'; // 型定義のみインポート

const worker = new Worker(new URL('./workers/heavy-comlink.ts', import.meta.url), {
  type: 'module',
});

// Worker を Comlink でラップ (型引数を指定するとIDEの補完が効きます)
const api = Comlink.wrap<HeavyApi>(worker);

// 普通の非同期関数として呼び出せる
const result = await api.fibonacci(40);
console.log(result);