Vite で Web Worker / SharedWorker を TypeScript でバンドルする初期設定
最近の Web アプリケーションでは、重い処理をメインスレッドから分離するために Web Worker や SharedWorker を活用する場面が増えてきました。 特に Vite と TypeScript を組み合わせた開発環境では、型安全性を保ちながら Worker を活用できるため、生産性の高い開発が可能です。
本記事では、Vite プロジェクトで Web Worker と SharedWorker を TypeScript でバンドルするための初期設定方法を、実際のコード例とともに丁寧に解説していきます。 設定から Worker の実装、型定義の活用まで、段階的に学んでいきましょう。
背景
Web Worker とは
Web Worker は、JavaScript をバックグラウンドスレッドで実行するための仕組みです。 メインスレッドとは別のスレッドで処理を実行するため、重い計算処理を行っても UI がブロックされることがありません。
SharedWorker は、複数のタブやウィンドウ間で同じ Worker インスタンスを共有できる特殊な Worker です。 これにより、タブ間でのデータ共有や通信が効率的に行えます。
Vite の特徴
Vite は、次世代のフロントエンド開発ツールとして注目されています。 開発時はネイティブ ES モジュールを活用し、本番環境では Rollup による最適化されたバンドルを生成するのが特徴です。
以下の図は、Vite プロジェクトにおける Worker の基本的な構成を示しています。
mermaidflowchart TB
main["メインスレッド<br/>(React/Vue等)"] -->|new Worker| worker["Web Worker<br/>(TypeScript)"]
main -->|new SharedWorker| shared["SharedWorker<br/>(TypeScript)"]
vite["Vite ビルドツール"] -->|バンドル| main
vite -->|バンドル| worker
vite -->|バンドル| shared
ts["tsconfig.json"] -->|型チェック| main
ts -->|型チェック| worker
ts -->|型チェック| shared
上記の図からわかるように、Vite はメインスレッドだけでなく、各 Worker ファイルも個別にバンドルし、TypeScript の型チェックも適用されます。
TypeScript と Worker の相性
TypeScript を使用することで、Worker 間のメッセージングに型安全性を持たせることができます。 これにより、開発時のエラーを早期に発見でき、リファクタリングも安全に行えるでしょう。
課題
Worker 利用時の一般的な課題
Worker を導入する際には、以下のような課題に直面することがあります。
| # | 課題 | 影響 |
|---|---|---|
| 1 | バンドル設定の複雑さ | Worker ファイルが正しくバンドルされない |
| 2 | TypeScript の型定義 | メッセージの型安全性が確保できない |
| 3 | モジュール解決 | Worker 内での import が動作しない |
| 4 | 開発体験の悪化 | ホットリロードが効かない |
これらの課題を解決するには、Vite の設定を適切に行う必要があります。
Vite 特有の注意点
Vite では、Worker ファイルを特別な方法でインポートする必要があります。 通常の import 文では、Worker として認識されずに通常のモジュールとしてバンドルされてしまうのです。
また、TypeScript の型定義ファイル(*.d.ts)の設定も重要になります。
型定義が不十分だと、せっかくの TypeScript の恩恵を受けられません。
下記の図は、Worker インポート時の処理フローを示しています。
mermaidflowchart LR
import["import文"] -->|通常| normal["通常モジュール<br/>として解決"]
import -->|?worker| workerCheck{"Worker<br/>クエリ検出"}
workerCheck -->|Web Worker| webWorker["Worker<br/>バンドル生成"]
workerCheck -->|Shared Worker| sharedWorker["SharedWorker<br/>バンドル生成"]
webWorker --> bundle["独立した<br/>バンドルファイル"]
sharedWorker --> bundle
この図が示すように、?workerというクエリパラメータを使うことで、Vite に「これは Worker ファイルだ」と認識させることができます。
解決策
プロジェクトの初期設定
まずは、Vite プロジェクトを作成し、必要なパッケージをインストールします。 この手順では、TypeScript テンプレートを使用してプロジェクトを初期化しましょう。
プロジェクトの作成
最初に Vite プロジェクトを作成します。
bashyarn create vite my-worker-app --template vanilla-ts
cd my-worker-app
yarn install
上記のコマンドで、TypeScript に対応した Vite プロジェクトが作成されます。
vanilla-tsテンプレートを使用していますが、React や Vue のテンプレートでも同様の設定が可能です。
必要なパッケージの確認
標準的な Vite プロジェクトには、以下のパッケージが含まれています。
json{
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}
Vite はデフォルトで Worker をサポートしているため、追加のプラグインは不要です。
TypeScript 設定
TypeScript で Worker を型安全に扱うための設定を行います。
tsconfig.jsonの設定は、型チェックの精度に直接影響するため重要です。
tsconfig.json の設定
プロジェクトルートにあるtsconfig.jsonを以下のように設定します。
json{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
この設定のポイントはlibオプションにWebWorkerを含めている点です。
これにより、Worker API の型定義が有効になります。
Worker 用の型定義ファイル
Worker で使用するメッセージの型を定義します。 型定義を分離することで、メインスレッドと Worker スレッド間での型の整合性を保てます。
src/types/worker.d.tsを作成し、以下のように記述します。
typescript// Workerに送信するメッセージの型定義
export interface WorkerRequest {
type: 'calculate' | 'process' | 'fetch';
payload: any;
}
// Workerから受信するメッセージの型定義
export interface WorkerResponse {
type: 'result' | 'error' | 'progress';
data: any;
}
上記の型定義により、メッセージの送受信時に型チェックが機能します。
次に、SharedWorker 用の型定義も追加しましょう。
typescript// SharedWorkerに送信するメッセージの型定義
export interface SharedWorkerRequest {
type: 'subscribe' | 'unsubscribe' | 'broadcast';
clientId: string;
payload?: any;
}
// SharedWorkerから受信するメッセージの型定義
export interface SharedWorkerResponse {
type: 'connected' | 'message' | 'error';
data: any;
}
これらの型定義は、後ほど Worker の実装で活用されます。
Vite 設定ファイル
Vite の設定ファイルで Worker 関連の設定を行います。 基本的にはデフォルト設定でも動作しますが、カスタマイズすることでより最適化できるでしょう。
vite.config.ts の設定
プロジェクトルートのvite.config.tsを以下のように設定します。
typescriptimport { defineConfig } from 'vite';
export default defineConfig({
worker: {
format: 'es',
plugins: [],
},
build: {
target: 'esnext',
},
});
worker.formatを'es'に設定することで、ES モジュール形式で Worker がバンドルされます。
これにより、最新の JavaScript 機能を活用できます。
必要に応じて、Worker ファイルのバンドルサイズを確認するロールアップオプションも追加できます。
typescriptimport { defineConfig } from 'vite';
export default defineConfig({
worker: {
format: 'es',
plugins: [],
rollupOptions: {
output: {
entryFileNames: 'workers/[name].[hash].js',
},
},
},
});
この設定により、Worker ファイルがworkers/ディレクトリ配下に出力されます。
具体例
Web Worker の実装
実際に Web Worker を実装し、メインスレッドから利用する方法を見ていきます。 ここでは、重い計算処理を Worker で実行する例を作成しましょう。
Worker ファイルの作成
src/workers/calculation.worker.tsを作成します。
typescriptimport type {
WorkerRequest,
WorkerResponse,
} from '../types/worker';
// Workerコンテキストの型定義
const ctx: Worker = self as any;
まず、Worker コンテキストを取得し、型を定義します。
selfは Worker グローバルスコープを指し、メインスレッドとは異なる実行環境です。
次に、メッセージハンドラを実装します。
typescript// メッセージ受信時の処理
ctx.addEventListener(
'message',
(event: MessageEvent<WorkerRequest>) => {
const { type, payload } = event.data;
try {
// リクエストタイプに応じた処理
if (type === 'calculate') {
const result = heavyCalculation(payload);
// 結果を返す
const response: WorkerResponse = {
type: 'result',
data: result,
};
ctx.postMessage(response);
}
} catch (error) {
// エラーハンドリング
const errorResponse: WorkerResponse = {
type: 'error',
data:
error instanceof Error
? error.message
: 'Unknown error',
};
ctx.postMessage(errorResponse);
}
}
);
上記のコードでは、受信したメッセージの型に応じて処理を分岐しています。
型定義により、event.dataの構造が明確になっているのがわかるでしょう。
重い計算処理の実装例を追加します。
typescript// 重い計算処理の例(フィボナッチ数列の計算)
function heavyCalculation(n: number): number {
if (n <= 1) return n;
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
const next = prev + curr;
prev = curr;
curr = next;
// 進捗を報告(10%ごと)
if (i % Math.floor(n / 10) === 0) {
const progress: WorkerResponse = {
type: 'progress',
data: Math.floor((i / n) * 100),
};
ctx.postMessage(progress);
}
}
return curr;
}
この関数では、計算の進捗も定期的に送信しています。 長時間かかる処理では、このような進捗報告が重要です。
メインスレッドでの利用
次に、メインスレッドから Worker を利用するコードを実装します。
src/main.tsを作成しましょう。
typescriptimport type {
WorkerRequest,
WorkerResponse,
} from './types/worker';
import CalculationWorker from './workers/calculation.worker?worker';
Worker ファイルをインポートする際、?workerクエリを付けることが重要です。
これにより、Vite が Worker として正しくバンドルします。
Worker のインスタンスを作成し、メッセージをやり取りする関数を実装します。
typescript// Workerインスタンスの作成
const worker = new CalculationWorker();
// Workerから結果を受け取る
worker.addEventListener(
'message',
(event: MessageEvent<WorkerResponse>) => {
const { type, data } = event.data;
if (type === 'result') {
console.log('計算結果:', data);
} else if (type === 'progress') {
console.log('進捗:', data + '%');
} else if (type === 'error') {
console.error('エラー:', data);
}
}
);
この実装により、Worker からの様々な種類のメッセージを適切に処理できます。
実際に計算を実行する関数を追加します。
typescript// Workerに計算を依頼する関数
function startCalculation(value: number): void {
const request: WorkerRequest = {
type: 'calculate',
payload: value,
};
worker.postMessage(request);
}
// エラーハンドリング
worker.addEventListener('error', (error: ErrorEvent) => {
console.error('Worker エラー:', error.message);
});
// 使用例
startCalculation(1000000);
型定義のおかげで、requestオブジェクトの構造が保証されています。
SharedWorker の実装
SharedWorker は、複数のタブやウィンドウ間で状態を共有する際に便利です。 実装方法は Web Worker と似ていますが、接続管理が必要になります。
SharedWorker ファイルの作成
src/workers/shared.worker.tsを作成します。
typescriptimport type {
SharedWorkerRequest,
SharedWorkerResponse,
} from '../types/worker';
// SharedWorkerコンテキストの型定義
const ctx: SharedWorkerGlobalScope = self as any;
// 接続中のポート一覧
const ports: MessagePort[] = [];
SharedWorker では、複数のクライアント(タブ)からの接続を管理する必要があります。
各接続はMessagePortとして管理されます。
接続時の処理を実装しましょう。
typescript// 新しい接続があった時の処理
ctx.addEventListener('connect', (event: MessageEvent) => {
const port = event.ports[0];
ports.push(port);
// 接続成功を通知
const response: SharedWorkerResponse = {
type: 'connected',
data: { clientCount: ports.length },
};
port.postMessage(response);
// このポートからのメッセージを受信
port.addEventListener(
'message',
(msgEvent: MessageEvent<SharedWorkerRequest>) => {
handleMessage(msgEvent.data, port);
}
);
port.start();
});
上記のコードでは、新しい接続が確立されるたびにポートを配列に追加し、メッセージハンドラを設定しています。
メッセージ処理とブロードキャスト機能を実装します。
typescript// メッセージ処理
function handleMessage(
data: SharedWorkerRequest,
senderPort: MessagePort
): void {
const { type, clientId, payload } = data;
if (type === 'broadcast') {
// 全てのクライアントにメッセージをブロードキャスト
ports.forEach((port) => {
const response: SharedWorkerResponse = {
type: 'message',
data: { from: clientId, message: payload },
};
port.postMessage(response);
});
}
}
この実装により、あるタブから送信されたメッセージを、接続中の全タブに配信できます。
切断時の処理も追加しましょう。
typescript// ポートのクリーンアップ
function removePort(port: MessagePort): void {
const index = ports.indexOf(port);
if (index !== -1) {
ports.splice(index, 1);
}
}
// ポートが閉じられた時の処理
ctx.addEventListener('message', (event: MessageEvent) => {
const port = event.ports[0];
port.addEventListener('close', () => {
removePort(port);
});
});
メインスレッドでの利用
SharedWorker をメインスレッドから利用するコードを実装します。
src/shared-main.tsを作成しましょう。
typescriptimport type {
SharedWorkerRequest,
SharedWorkerResponse,
} from './types/worker';
import SharedWorkerInstance from './workers/shared.worker?worker';
SharedWorker も Web Worker と同様に?workerクエリを使ってインポートします。
SharedWorker のインスタンスを作成し、接続を確立します。
typescript// SharedWorkerインスタンスの作成
const sharedWorker = new SharedWorkerInstance();
const port = sharedWorker.port;
// 一意のクライアントIDを生成
const clientId = crypto.randomUUID();
// メッセージ受信の設定
port.addEventListener(
'message',
(event: MessageEvent<SharedWorkerResponse>) => {
const { type, data } = event.data;
if (type === 'connected') {
console.log(
'SharedWorkerに接続しました。クライアント数:',
data.clientCount
);
} else if (type === 'message') {
console.log(
`${data.from}からのメッセージ:`,
data.message
);
}
}
);
// ポートを開始
port.start();
SharedWorker ではport.start()の呼び出しが必須です。
これを忘れると、メッセージの受信が開始されません。
ブロードキャスト機能を使う関数を実装します。
typescript// 全クライアントにメッセージを送信
function broadcast(message: string): void {
const request: SharedWorkerRequest = {
type: 'broadcast',
clientId: clientId,
payload: message,
};
port.postMessage(request);
}
// エラーハンドリング
sharedWorker.addEventListener(
'error',
(error: ErrorEvent) => {
console.error('SharedWorker エラー:', error.message);
}
);
// 使用例
broadcast('こんにちは、他のタブの皆さん!');
この実装により、複数のタブ間でリアルタイムにメッセージを共有できます。
実践的な使用例
実際のアプリケーションで Worker を活用する例を見てみましょう。 ここでは、データ処理とキャッシュ管理を行う Worker システムを構築します。
データ処理用 Worker の実装
src/workers/data-processor.worker.tsを作成します。
typescriptinterface DataItem {
id: number;
value: number;
timestamp: Date;
}
const ctx: Worker = self as any;
データアイテムの型を定義し、Worker コンテキストを設定します。
データ処理のロジックを実装しましょう。
typescript// 大量データの集計処理
function aggregateData(
items: DataItem[]
): Record<string, number> {
const result = {
total: 0,
average: 0,
min: Infinity,
max: -Infinity,
count: items.length,
};
for (const item of items) {
result.total += item.value;
result.min = Math.min(result.min, item.value);
result.max = Math.max(result.max, item.value);
}
result.average = result.total / result.count;
return result;
}
この関数は、大量のデータアイテムから統計情報を計算します。 メインスレッドで実行すると UI がフリーズする可能性がある処理です。
メッセージハンドラを追加します。
typescriptctx.addEventListener('message', (event: MessageEvent) => {
const { type, payload } = event.data;
if (type === 'aggregate') {
const data = payload as DataItem[];
const result = aggregateData(data);
ctx.postMessage({
type: 'aggregated',
data: result,
});
}
});
HTML での実装例
実際の HTML ファイルで Worker を使用する例を示します。
index.htmlを作成しましょう。
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Vite Worker サンプル</title>
</head>
<body>
<div id="app">
<h1>Worker計算サンプル</h1>
<button id="calculate-btn">重い計算を実行</button>
<div id="progress"></div>
<div id="result"></div>
<hr />
<h2>SharedWorker通信</h2>
<input
type="text"
id="message-input"
placeholder="メッセージを入力"
/>
<button id="broadcast-btn">送信</button>
<div id="messages"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
この HTML では、Worker を使った計算と SharedWorker を使った通信の両方を試せます。
UI と Worker を連携させるコードをsrc/main.tsに追加します。
typescript// DOM要素の取得
const calculateBtn = document.getElementById(
'calculate-btn'
) as HTMLButtonElement;
const progressDiv = document.getElementById(
'progress'
) as HTMLDivElement;
const resultDiv = document.getElementById(
'result'
) as HTMLDivElement;
// ボタンクリック時の処理
calculateBtn?.addEventListener('click', () => {
progressDiv.textContent = '計算中...';
resultDiv.textContent = '';
startCalculation(1000000);
});
// Worker応答の更新処理
worker.addEventListener(
'message',
(event: MessageEvent<WorkerResponse>) => {
const { type, data } = event.data;
if (type === 'progress') {
progressDiv.textContent = `進捗: ${data}%`;
} else if (type === 'result') {
progressDiv.textContent = '完了!';
resultDiv.textContent = `計算結果: ${data}`;
}
}
);
これにより、Worker の処理状況がリアルタイムで UI に反映されます。
以下の図は、実装したシステム全体の動作フローを示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant UI as メインスレッド<br/>(UI)
participant WW as Web Worker<br/>(計算処理)
participant SW as SharedWorker<br/>(通信管理)
User->>UI: ボタンクリック
UI->>WW: 計算リクエスト
WW->>UI: 進捗通知(10%)
WW->>UI: 進捗通知(50%)
WW->>UI: 進捗通知(100%)
WW->>UI: 計算結果
UI->>User: 結果表示
User->>UI: メッセージ入力
UI->>SW: ブロードキャスト
SW->>UI: 全タブに配信
UI->>User: メッセージ表示
この図が示すように、UI は Worker との非同期通信により、応答性を保ちながら重い処理を実行できます。
ビルドと動作確認
実装した Worker が正しく動作するか、開発サーバーとビルドの両方で確認しましょう。
開発サーバーの起動
開発環境で Worker をテストします。
bashyarn dev
上記のコマンドで開発サーバーが起動し、http://localhost:5173でアプリケーションにアクセスできます。
Worker ファイルは自動的に別ファイルとしてバンドルされます。
プロダクションビルド
本番環境用のビルドを実行します。
bashyarn build
ビルドが完了すると、dist/ディレクトリにバンドルされたファイルが生成されます。
Worker ファイルは独立した JS ファイルとして出力されているはずです。
ビルド結果を確認するコマンドを実行しましょう。
bashls -la dist/
ls -la dist/workers/
workers/ディレクトリ内に、ハッシュ付きの Worker ファイルが生成されていることを確認できます。
ビルド結果のプレビュー
ビルドしたアプリケーションをローカルでプレビューします。
bashyarn preview
プレビューサーバーが起動し、本番環境と同じ構成でアプリケーションの動作を確認できます。 ブラウザの開発者ツールの Network タブで、Worker ファイルが正しく読み込まれているか確認しましょう。
まとめ
Vite と TypeScript を組み合わせることで、Web Worker と SharedWorker を型安全に活用できる開発環境を構築できました。
本記事で解説した内容を振り返ると、以下のポイントが重要です。
| # | ポイント | 詳細 |
|---|---|---|
| 1 | Worker インポート | ?workerクエリを使用して Vite に認識させる |
| 2 | 型定義 | メッセージの型を定義して型安全性を確保 |
| 3 | Vite 設定 | vite.config.tsで Worker のビルド形式を指定 |
| 4 | TypeScript 設定 | tsconfig.jsonにWebWorkerライブラリを追加 |
| 5 | エラーハンドリング | Worker 内外の両方でエラー処理を実装 |
Worker を使うことで、重い処理をバックグラウンドで実行し、快適なユーザー体験を提供できます。 SharedWorker を活用すれば、複数タブ間でのリアルタイム通信も実現可能です。
TypeScript の型システムにより、Worker とのメッセージングも安全に実装でき、リファクタリングも容易になりました。 今回の設定を基に、より複雑な Worker システムを構築していけるでしょう。
ぜひ、実際のプロジェクトで Worker を活用し、パフォーマンスの向上を体感してみてください。
関連リンク
articleVite で Web Worker / SharedWorker を TypeScript でバンドルする初期設定
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
articleVite CSS HMR が反映されない時のチェックリスト:PostCSS/Modules/Cache 編
articleesbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
articleVite 本番の可観測性:ソースマップアップロードと Sentry 連携でエラーを特定
articleMicro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築
articleReact クリーンアーキテクチャ実践:UI・アプリ・ドメイン・データの責務分離
articleWebLLM vs サーバー推論 徹底比較:レイテンシ・コスト・スケールの実測レポート
articleVitest モック技術比較:MSW / `vi.mock` / 手動スタブ — API テストの最適解はどれ?
articlePython ORMs 実力検証:SQLAlchemy vs Tortoise vs Beanie の選び方
articleVite で Web Worker / SharedWorker を TypeScript でバンドルする初期設定
articlePrisma Accelerate と PgBouncer を比較:サーバレス時代の接続戦略ベンチ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来