Web Workers:UIをフリーズさせずに重いタスクを処理する秘訣

Development tutorial - IT technology blog
Development tutorial - IT technology blog

「ページが応答しません」という悪夢とJavaScriptの限界

ユーザーが「レポート出力」ボタンをクリックした瞬間にWebサイトが固まってしまった経験はありませんか?画面がフリーズし、マウスカーソルが回転し続け、ブラウザが忌々しい 「ページが応答しません」 という通知を表示する。これはメインスレッド(Main Thread)が過負荷になった際の必然的な結果です。

本質的に、JavaScriptはシングルスレッド(Single-threaded)で動作します。UIのレンダリングからクリックイベントの取得、ロジックの計算まで、すべてが1つの道に並んで処理を待ちます。もし100万要素の配列を処理させようとすれば、その道は渋滞してしまいます。ブラウザにはUIを更新するためのリソースが残らず、ユーザー体験は最悪なものになります。

Web Workersこそがその救世主です。これはバックグラウンドスレッド(Background Thread)という「優先車線」を新たに作成することを可能にします。重いタスクはバックグラウンド処理させ、その間メインスレッドはユーザーのスクロールやクリック操作を軽快に処理し続けることができます。

Web Workersの実装:スレッドを分離してUIを解放する

嬉しいことに、Web Workersは標準APIとして提供されているため、重いライブラリを追加でインストールする必要はありません。ただし、Workerは完全に独立した環境で動作するため、コードを別のファイルに分ける必要があります。

Workerにデータを渡す前に、私はよく JSON Formatter を使って構造をチェックします。これにより、入力データが正しいことを確認し、Workerが途中でクラッシュするような単純なロジックエラーを防ぐことができます。

メインスレッド(main.js)とバックグラウンド処理(worker.js)の間の基本的な設定方法を見てみましょう。

ステップ1:main.jsでサブスレッドを初期化する

// main.js
if (window.Worker) {
    const myWorker = new Worker('worker.js');

    // 500,000件のレコードをバックグラウンド処理に送る
    myWorker.postMessage(hugeDataset);

    // Workerが完了した時に結果を受け取る
    myWorker.onmessage = (e) => {
        console.log('データ処理が完了しました:', e.data);
        renderUI(e.data);
    };

    myWorker.onerror = (err) => console.error('Workerエラー:', err.message);
}

ステップ2:worker.jsに処理ロジックを記述する

重要な注意点:Worker内ではDOM(document, window)にアクセスできません。すべての操作は純粋なデータ処理のみとなります。

// worker.js
onmessage = function(e) {
    const data = e.data;
    
    // 重い処理のシミュレーション:100万アイテムをマップ処理
    const result = data.map(item => {
        let sum = 0;
        for(let i = 0; i < 1000; i++) sum += i;
        return { ...item, score: sum };
    });

    postMessage(result);
};

最適化:データ転送時のパフォーマンスの罠を避ける

デフォルトでは、ブラウザはスレッド間でデータをコピーするために Structured Clone アルゴリズムを使用します。もし100MBのJSONファイルを送信する場合、ブラウザは同じコピーを作成するだけでかなりのCPUとメモリを消費します。これが意図せず新たなボトルネックを生むことになります。

Transferable Objectsを使用して最高速度を実現する

バイト配列(ArrayBuffer)形式のデータでは、「所有権の譲渡」という仕組みを利用しましょう。コピーする代わりに、メインスレッドはメモリ領域のアドレスを直接Workerに渡します。譲渡後、メインスレッドはアクセス権を失いますが、転送速度はほぼ0msになります。

// コピーのリソースを消費せずに32MBのデータを譲渡する
const buffer = new ArrayBuffer(1024 * 1024 * 32);
myWorker.postMessage(buffer, [buffer]);

効果の検証:数字は嘘をつかない

パフォーマンスを推測するのではなく、Chrome DevToolsを開いて明確な違いを確認しましょう。Performance タブでは、Workerを使用する前後で劇的な変化が見られます。

  • Workerなし: Main行に長い赤線が表示され、「Long Task」(通常50ms以上)を警告します。Webサイトがカクつきます。
  • Workerあり: Main行は空いており、クリーンな状態です。重い計算ブロックは「Worker」という別の行に押し出されています。

実務上の経験から言うと、Web Workerは100ms以上かかるタスクにのみ使用すべきです。Worker의初期化にも一定のコスト(オーバーヘッド)がかかります。単純な数値をいくつか加算するだけなら、メインスレッドで直接実行する方が速い場合もあります。

大規模なプロジェクトでWorkerをよりプロフェッショナルに管理したい場合は、Comlink を試してみてください。このライブラリは、複雑な postMessage のやり取りを非常にスッキリとした async/await の関数呼び出しに変換してくれます。Web Workerは単なるツールではなく、計算と表示を切り離してプロフェッショナルなWebアプリケーションを作成するための思考法なのです。

Share: