こんにちは!日々のフロントエンド開発、本当にお疲れ様です。
突然ですが、こんな経験はありませんか?
「ユーザーがボタンを1回クリックしただけなのに、数千件もある巨大なデータを画面上でソートし始めた途端、画面がピクリとも動かなくなった……」
「チャートを描画する重い計算のせいで、アニメーションがカクカクしてユーザー体験(UX)が最悪になってしまった……」
ブラウザのJavaScriptは、基本的に「シングルスレッド(=1つの作業台)」で動いています。つまり、メインの作業台の上で重い計算(大根の千切り)をしている間は、ユーザーが画面をスクロールしたり(お皿を洗ったり)、ボタンを押したり(電話に出たり)する作業がすべてストップしてしまうのです。
これを解決するのがWeb Workerです。そして、次世代の超高速ビルドツールであるViteを使えば、このWeb Workerを設定ファイル一切なしのネイティブ(標準)の力で、驚くほど簡単かつ爆速で導入できます。
今回は、ViteのWorker Threads機能をフル活用し、重いフロントエンド計算をメインスレッドから綺麗に分離して、アプリをピカピカに爆速化する実践テクニックを優しく丁寧に解説します。これをマスターすれば、あなたの作るWebアプリは見違えるほど滑らかになり、ユーザーから「おっ、このアプリ快適だな!」と感動されること間違いなしです。一緒に見ていきましょう!
—
1. ViteにおけるWeb Workerの役割と、なぜ今「Worker Threads」なのか?
従来のWebpackなどでWeb Workerを使おうとすると、`worker-loader`という専用のローダーを導入したり、複雑なパス解決のwebpack設定を書いたりする必要がありました。「Workerを使いたいだけなのに、なんでこんなに設定ファイルと格闘しなきゃいけないんだ……」と挫折したエンジニアも多いはずです。
しかし、Viteは違います。ViteはブラウザのネイティブESモジュール(ESM)の仕組みを最大限に活かしているため、追加のプラグインなしで、そのままコードを書くだけでWorkerをビルド・バンドルしてくれるのです。
メインスレッドとWorkerスレッドの関係
- メインスレッド: 画面の描画(DOM操作)、ユーザーからの入力受付(クリックやタイピング)を担当する「表舞台」。ここを重い計算で占拠してはいけない。
- Workerスレッド: 画面を持たないバックグラウンドの「裏方作業員」。ここで重い計算(数百万回のループ処理、画像・データのパースなど)を丸ごと引き受けてもらう。
Viteを使えば、この2つのスレッド間の通信が驚くほどスムーズになります。それでは、実際に手を動かしながらその威力を体感していきましょう!
—
2. プロジェクトの準備と最も重要な基礎セットアップ
まずは、Viteを使ったモダンな開発環境をサクッと立ち上げます。ターミナルを開き、以下のコマンドを順番に実行してください。
1. Viteを使ったVanilla TypeScriptプロジェクトの作成(名前は vite-worker-demo とします)
npm create vite@latest vite-worker-demo — –template vanilla-ts
2. 作成したディレクトリへ移動
cd vite-worker-demo
3. 依存パッケージのインストール
npm install
プロジェクトが作成できたら、エディタ(VSCodeなど)で開いてみてください。
Viteのプロジェクト構成は非常にシンプルですが、今回はここに「重い計算を担うWorkerファイル」と「それを呼び出すメインの処理」を追加していきます。
—
3. 実践!HelloWorldを超える「重い計算の分離」実装
今回は、分かりやすく「1から10億までの数値を足し算する(=わざとCPUに負荷をかける重い処理)」を題材にします。これをメインスレッドでやると確実に画面がフリーズしますが、Workerに投げるとどうなるでしょうか。
① ワーカーファイルの作成 (`src/heavy-worker.ts`)
まずは、バックグラウンドで黙々と重い計算をする裏方スタッフを作ります。`src` ディレクトリの中に `heavy-worker.ts` というファイルを作成してください。
// src/heavy-worker.ts
// メインスレッドからメッセージ(データ)を受け取ったときのイベントリスナー
self.onmessage = (event: MessageEvent) => {
const limit = event.data.limit || 1000000000;
console.log(`[Worker] 計算を開始します(上限: ${limit})…`);
const startTime = performance.now();
let sum = 0;
// あえてCPUに負荷をかける重いループ処理
for (let i = 1; i <= limit; i++) {
sum += i;
}
const endTime = performance.now();
const duration = (endTime - startTime).toFixed(2);
console.log(`[Worker] 計算完了! 経過時間: ${duration}ms`);
// 計算結果をメインスレッドへ送り返す
self.postMessage({
sum,
duration,
});
};
- 解説: `self.onmessage` でメインスレッドからの合図を待ち、計算が終わったら `self.postMessage` で結果を押し戻す。これがWorkerの基本の形です。
—
② メインスレッド側の実装 (`src/main.ts`)
次に、画面のUIと、Workerを呼び出す司令塔となるメインスレッドを作ります。既存の `src/main.ts` を以下のように書き換えてください。
// src/main.ts
import ‘./style.css’
// 画面のHTML構造を構築
document.querySelector
Vite Worker Threads Demo
メインスレッドのフリーズを防ぎながら重い計算を実行します
`
// 1. カウンターボタンの動作確認(これがフリーズしないことが重要!)
let count = 0;
const counterBtn = document.querySelector
counterBtn.addEventListener(‘click’, () => {
count++;
counterBtn.innerText = `カウント: ${count}`;
});
// 2. ViteのネイティブWeb Worker機能を使ったインスタンス化
// 【超重要】 “?worker” というサフィックスをつけるだけで、Viteが自動的にWorkerとしてバンドルしてくれます!
const worker = new Worker(new URL(‘./heavy-worker.ts’, import.meta.url), {
type: ‘module’, // ES Modules形式のWorkerとして読み込む
});
const calcBtn = document.querySelector
const resultDiv = document.querySelector
// 3. 重い計算ボタンが押されたときの処理
calcBtn.addEventListener(‘click’, () => {
calcBtn.setAttribute(‘disabled’, ‘true’);
calcBtn.innerText = ‘計算中… (UIはフリーズしません!)’;
resultDiv.innerText = ‘バックグラウンドで計算中…’;
// Workerへ計算開始のデータを送信
worker.postMessage({ limit: 2000000000 }); // 20億回ループ!
});
// 4. Workerから計算結果を受け取ったときの処理
worker.onmessage = (event: MessageEvent) => {
const { sum, duration } = event.data;
resultDiv.innerText = `計算結果: ${sum} (所要時間: ${duration} ms)`;
calcBtn.removeAttribute(‘disabled’);
calcBtn.innerText = ‘重い計算を実行する (Worker)’;
};
—
③ 動作確認をしてみよう!
それでは、開発サーバーを起動して実際に動かしてみましょう。
npm run dev
ブラウザで `http://localhost:5173` (通常はポート5173)を開いてください。
1. 「カウント: 0」ボタンを何度か連打してみてください。 スムーズに数字が増えていきますよね。
2. 次に、「重い計算を実行する (Worker)」ボタンを押してください。
3. 计算が裏方で走っている間も、もう一度「カウント」ボタンを連打してみてください。
どうですか? 計算の最中であっても、カウントボタンの数字は一切カクつくことなく、サクサクと増えていくはずです!
もしこれがWorkerを使わずメインスレッドだけで計算していたら、計算が始ま算と同時にボタンすら押せなくなる(画面が固まる)現象が起きています。この「UIが絶対に死なない快適さ」こそが、Worker Threads導入の最大の果実です。
—
4. アーキテクトが教える、実務で絶対に外せないベストプラクティス
このViteのWorker機能、実務のプロダクション環境に導入する際には、いくつかの重要な設計上の注意点(ベストプラクティス)があります。これを知っているだけで、トラブルを未然に防ぎ、コードの品質が劇的に向上します。
1. データの受け渡しは「コピー」または「転送(Transferable)」を意識する
メインスレッドとWorkerの間でデータをやり取りする際、デフォルトではデータがディープコピー(複製)されます。数MB〜数百MBもある巨大な配列やArrayBufferを送受信する場合、このコピーコスト自体がボトルネックになることがあります。
もしメモリのコピーを発生させたくない場合は、Transferable Objects(`transfer` オプション)を使いましょう。
// 例: ArrayBufferのオーナー権限自体をWorkerに丸ごと「移動」させる(コピーゼロで爆速)
const buffer = new ArrayBuffer(1024 1024 50); // 50MB
worker.postMessage({ buffer }, { transfer: [buffer] });
// ※注意: transferした後のbufferはメインスレッド側では空っぽになります
2. エラーハンドリングを必ず実装する
裏方のWorker内で予期せぬエラー(ゼロ除算や未定義プロパティへのアクセスなど)が発生した場合、デフォルトだとサイレントに失敗するか、コンソールに分かりにくいエラーが出るだけになります。メインスレッド側で `onerror` を必ずキャッチできるようにしましょう。
worker.onerror = (error) => {
console.error(‘Workerの内部でエラーが発生しました:’, error.message);
// ユーザーへの通知やフォールバック処理をここに書く
};
3. 使い終わったWorkerは適切に破棄(Terminate)する
コンポーネントのアンマウント時や、不要になったWorkerは、メモリリークを防ぐために明示的に終了させましょう。
// もうWorkerが不要になったとき
worker.terminate();
—
おわりに
今回は、ViteのネイティブなWeb Worker(Worker Threads)機能を使って、重い計算をメインスレッドから綺麗に分離し、アプリのUIを爆速に保つ手法を解説しました。
複雑な設定ファイルを書く必要もなく、`new URL(…, import.meta.url)` と `?worker` サフィックスを組み合わせるだけで、モダンブラウザのマルチコア性能を最大限に引き出すことができます。
「重い処理=メインスレッドをフリーズさせる原因」という常識を、今日からぜひViteのWorkerで覆してください。あなたの開発するWebアプリケーションが、ユーザーにとって最高に心地よい体験を提供できるようになることを、心から応援しています!