JavaScript Web Workers 実践入門:重い処理を別スレッドへ逃がす最短手順

Web アプリケーションを開発していると、大量のデータ処理や複雑な計算によって画面が固まってしまった経験はありませんか。そんな時に活躍するのが Web Workers です。この技術を使えば、重い処理を別スレッドに移して、メインの UI を快適に保つことができます。
本記事では、Web Workers の基本的な仕組みから実際の実装手順まで、初心者の方にもわかりやすく解説いたします。記事を読み終える頃には、あなたも自信を持って Web Workers を活用できるようになるでしょう。
背景
JavaScript のシングルスレッド問題
JavaScript は基本的に シングルスレッド で動作します。これは、一度に一つの処理しか実行できないことを意味しています。通常の Web ページでは問題になりませんが、複雑な処理が必要になると大きな制約となってしまいます。
以下の図は、JavaScript のシングルスレッド処理の流れを示しています。
mermaidflowchart TD
start["処理開始"] --> task1["軽い処理1"]
task1 --> heavy["重い処理<br/>(数秒かかる)"]
heavy --> task2["軽い処理2"]
task2 --> ui_update["UI更新"]
ui_update --> done["処理完了"]
heavy -.->|ブロック| ui_block["UI操作不可<br/>画面固着"]
ui_block -.-> heavy
図で理解できる要点:
- 重い処理の実行中は他の処理が一切実行されない
- UI 操作やアニメーションも完全に停止してしまう
- 処理が完了するまでユーザーは何もできない状態になる
UI ブロッキングの課題
シングルスレッドの性質により、以下のような問題が発生します。
問題 | 具体的な影響 | ユーザー体験への影響 |
---|---|---|
画面の固着 | クリックやスクロールが効かない | 操作不能でストレス |
アニメーション停止 | ローディング表示も止まる | アプリが壊れたと誤解 |
応答性の低下 | 数秒間の完全停止 | 使いにくさを感じる |
パフォーマンス改善の必要性
現代の Web アプリケーションでは、以下のような重い処理が求められることが多くなっています。
- 大量データの解析:数万件の CSV ファイル処理
- 複雑な計算:グラフィック処理や統計計算
- リアルタイム処理:チャットアプリでの暗号化処理
- 画像・動画処理:フィルター適用や圧縮処理
これらの処理をメインスレッドで実行すると、必ずユーザビリティが低下してしまいます。だからこそ、Web Workers という解決策が重要になってくるのです。
課題
メインスレッドでの重い処理による画面フリーズ
従来の JavaScript 開発では、すべての処理をメインスレッドで実行していました。これにより、以下のような深刻な問題が発生していました。
javascript// 問題のあるコード例:メインスレッドでの重い処理
function heavyCalculation() {
console.log('計算開始...');
// 1億回のループ処理(意図的に重い処理)
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += Math.sqrt(i);
}
console.log('計算完了:', result);
return result;
}
// この関数を呼び出すと画面が数秒間固まる
document
.getElementById('calc-button')
.addEventListener('click', () => {
heavyCalculation(); // UI完全停止
alert('処理が完了しました'); // 計算完了まで表示されない
});
上記のコードの問題点:
- 計算中はすべての UI 操作が無効になる
- ローディング表示すら更新されない
- ユーザーはアプリがクラッシュしたと誤解する可能性がある
ユーザビリティの低下
重い処理による UI ブロッキングは、以下のような具体的なユーザビリティ問題を引き起こします。
問題の種類 | 発生場面 | ユーザーの反応 |
---|---|---|
無応答状態 | データ処理中のクリック | 「壊れた?」と不安 |
進捗不明 | 長時間処理の実行中 | 「いつ終わる?」とイライラ |
操作不能 | 処理中の他機能利用 | 「使いにくい」と評価低下 |
従来の解決策の限界
これまで、開発者は以下のような方法で問題を解決しようとしてきました。
javascript// 従来の解決策1: setTimeoutによる分割処理
function heavyCalculationWithTimeout(data, callback) {
let index = 0;
const batchSize = 1000;
function processBatch() {
const endIndex = Math.min(
index + batchSize,
data.length
);
// バッチ処理
for (let i = index; i < endIndex; i++) {
// 何らかの処理
processItem(data[i]);
}
index = endIndex;
if (index < data.length) {
// 次のバッチを非同期で実行
setTimeout(processBatch, 0);
} else {
callback();
}
}
processBatch();
}
しかし、この方法にも以下の限界がありました:
- 処理が複雑になる:コードの可読性が大幅に低下
- デバッグが困難:非同期処理の追跡が難しい
- パフォーマンス限界:根本的な解決にならない
- メモリ使用量の増加:分割処理による overhead
これらの限界を克服するために、Web Workers という新しいアプローチが生まれました。
解決策
Web Workers の仕組み
Web Workers は、JavaScript の処理を 別スレッド で実行する仕組みです。これにより、重い処理をバックグラウンドで実行しながら、メインスレッドの UI を快適に保つことができます。
以下の図は、Web Workers を使った処理の流れを示しています。
mermaidflowchart LR
subgraph main["メインスレッド"]
ui["UI処理"] --> message_send["メッセージ送信"]
message_receive["メッセージ受信"] --> ui_update["UI更新"]
end
subgraph worker["Workerスレッド"]
receive["メッセージ受信"] --> heavy_calc["重い計算処理"]
heavy_calc --> send["結果送信"]
end
message_send -->|postMessage| receive
send -->|postMessage| message_receive
style main fill:#e1f5fe
style worker fill:#f3e5f5
図で理解できる要点:
- メインスレッドと Worker スレッドが並行して動作
- メッセージパッシングによる安全な通信
- UI は常にレスポンシブな状態を維持
基本的な使い方
Web Workers の実装は、以下の 3 つのステップで完了します。
ステップ 1: Worker ファイルの作成
まず、別スレッドで実行したい処理を記述した JavaScript ファイルを作成します。
javascript// worker.js - Worker側のコード
self.addEventListener('message', function (event) {
const { type, data } = event.data;
if (type === 'HEAVY_CALCULATION') {
// 重い計算処理
const result = performHeavyCalculation(data);
// 結果をメインスレッドに送信
self.postMessage({
type: 'CALCULATION_COMPLETE',
result: result,
});
}
});
function performHeavyCalculation(data) {
let result = 0;
for (let i = 0; i < data.iterations; i++) {
result += Math.sqrt(i) * Math.cos(i);
}
return result;
}
ステップ 2: メインスレッドで Worker を起動
次に、メインスレッドから Worker を作成し、処理を依頼します。
javascript// main.js - メインスレッドのコード
class WorkerManager {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.setupEventListeners();
}
setupEventListeners() {
this.worker.addEventListener('message', (event) => {
const { type, result } = event.data;
if (type === 'CALCULATION_COMPLETE') {
this.handleCalculationComplete(result);
}
});
this.worker.addEventListener('error', (error) => {
console.error('Worker error:', error);
});
}
startCalculation(iterations) {
this.worker.postMessage({
type: 'HEAVY_CALCULATION',
data: { iterations },
});
}
handleCalculationComplete(result) {
document.getElementById(
'result'
).textContent = `結果: ${result}`;
document.getElementById('loading').style.display =
'none';
}
}
ステップ 3: UI との連携
最後に、ユーザーの操作と連携させます。
javascript// UI連携のコード
document.addEventListener('DOMContentLoaded', () => {
const workerManager = new WorkerManager('./worker.js');
const calcButton = document.getElementById('calc-button');
const loadingElement = document.getElementById('loading');
calcButton.addEventListener('click', () => {
// UIの状態を更新
loadingElement.style.display = 'block';
calcButton.disabled = true;
// 重い処理を開始(非ブロッキング)
workerManager.startCalculation(100000000);
// この時点でもUIは操作可能
console.log('処理開始 - UIは引き続き操作可能です');
});
});
メッセージパッシングの概念
Web Workers では、メインスレッドと Worker スレッド間で直接変数を共有することはできません。代わりに メッセージパッシング という仕組みを使って、安全にデータをやり取りします。
javascript// メッセージパッシングの基本パターン
// メインスレッド → Worker
worker.postMessage({
command: 'PROCESS_DATA',
payload: { items: [1, 2, 3, 4, 5] },
});
// Worker → メインスレッド
self.postMessage({
status: 'SUCCESS',
result: processedData,
});
この仕組みの利点:
- データの安全性:意図しない変更を防止
- スレッドセーフ:競合状態の回避
- 明確な責任分離:処理の流れが理解しやすい
具体例
シンプルな計算処理の例
最初に、基本的な数値計算を Web Workers で実装してみましょう。素数を見つける処理を例に、具体的な実装を見ていきます。
HTML 構造
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Web Workers 素数計算デモ</title>
</head>
<body>
<h1>素数計算デモ</h1>
<div>
<label for="max-number">最大値:</label>
<input
type="number"
id="max-number"
value="100000"
min="1000"
/>
</div>
<button id="calc-button">素数を計算</button>
<button id="cancel-button" disabled>キャンセル</button>
<div id="progress-container" style="display: none;">
<div>進捗: <span id="progress-text">0%</span></div>
<progress
id="progress-bar"
value="0"
max="100"
></progress>
</div>
<div id="result-container" style="display: none;">
<h3>結果</h3>
<p>
発見した素数の数: <span id="prime-count"></span>
</p>
<p>処理時間: <span id="execution-time"></span>ms</p>
</div>
</body>
</html>
Worker ファイル(prime-worker.js)
素数計算を行う Worker 側のコードです。進捗報告機能も含めています。
javascript// prime-worker.js
self.addEventListener('message', function (event) {
const { type, data } = event.data;
switch (type) {
case 'FIND_PRIMES':
findPrimes(data.maxNumber);
break;
case 'CANCEL':
self.close(); // Workerを終了
break;
}
});
function findPrimes(maxNumber) {
const primes = [];
const startTime = Date.now();
let shouldCancel = false;
// キャンセル要求の監視
self.addEventListener('message', function (event) {
if (event.data.type === 'CANCEL') {
shouldCancel = true;
}
});
for (
let num = 2;
num <= maxNumber && !shouldCancel;
num++
) {
if (isPrime(num)) {
primes.push(num);
}
// 進捗報告(1000件ごと)
if (num % 1000 === 0) {
const progress = (num / maxNumber) * 100;
self.postMessage({
type: 'PROGRESS',
data: {
progress: progress,
currentNumber: num,
primesFound: primes.length,
},
});
}
}
const endTime = Date.now();
if (!shouldCancel) {
// 完了通知
self.postMessage({
type: 'COMPLETE',
data: {
primes: primes,
count: primes.length,
executionTime: endTime - startTime,
},
});
}
}
function isPrime(n) {
if (n < 2) return false;
if (n === 2) return true;
if (n % 2 === 0) return false;
for (let i = 3; i <= Math.sqrt(n); i += 2) {
if (n % i === 0) return false;
}
return true;
}
メインスレッド(main.js)
UI の制御と Worker との通信を管理するコードです。
javascript// main.js
class PrimeCalculator {
constructor() {
this.worker = null;
this.isCalculating = false;
this.initializeElements();
this.setupEventListeners();
}
initializeElements() {
this.elements = {
maxNumberInput: document.getElementById('max-number'),
calcButton: document.getElementById('calc-button'),
cancelButton:
document.getElementById('cancel-button'),
progressContainer: document.getElementById(
'progress-container'
),
progressText:
document.getElementById('progress-text'),
progressBar: document.getElementById('progress-bar'),
resultContainer: document.getElementById(
'result-container'
),
primeCount: document.getElementById('prime-count'),
executionTime: document.getElementById(
'execution-time'
),
};
}
setupEventListeners() {
this.elements.calcButton.addEventListener(
'click',
() => {
this.startCalculation();
}
);
this.elements.cancelButton.addEventListener(
'click',
() => {
this.cancelCalculation();
}
);
}
startCalculation() {
const maxNumber = parseInt(
this.elements.maxNumberInput.value
);
if (maxNumber < 1000) {
alert('1000以上の数値を入力してください');
return;
}
this.isCalculating = true;
this.updateUI('calculating');
// 新しいWorkerを作成
this.worker = new Worker('./prime-worker.js');
this.setupWorkerListeners();
// 計算開始
this.worker.postMessage({
type: 'FIND_PRIMES',
data: { maxNumber },
});
}
setupWorkerListeners() {
this.worker.addEventListener('message', (event) => {
const { type, data } = event.data;
switch (type) {
case 'PROGRESS':
this.updateProgress(data);
break;
case 'COMPLETE':
this.handleComplete(data);
break;
}
});
this.worker.addEventListener('error', (error) => {
console.error('Worker error:', error);
this.handleError(error);
});
}
updateProgress(data) {
this.elements.progressText.textContent = `${data.progress.toFixed(
1
)}% (素数: ${data.primesFound}個)`;
this.elements.progressBar.value = data.progress;
}
handleComplete(data) {
this.elements.primeCount.textContent =
data.count.toLocaleString();
this.elements.executionTime.textContent =
data.executionTime.toLocaleString();
this.isCalculating = false;
this.updateUI('complete');
this.cleanupWorker();
}
cancelCalculation() {
if (this.worker && this.isCalculating) {
this.worker.postMessage({ type: 'CANCEL' });
this.cleanupWorker();
this.isCalculating = false;
this.updateUI('idle');
}
}
updateUI(state) {
switch (state) {
case 'calculating':
this.elements.calcButton.disabled = true;
this.elements.cancelButton.disabled = false;
this.elements.progressContainer.style.display =
'block';
this.elements.resultContainer.style.display =
'none';
break;
case 'complete':
this.elements.calcButton.disabled = false;
this.elements.cancelButton.disabled = true;
this.elements.progressContainer.style.display =
'none';
this.elements.resultContainer.style.display =
'block';
break;
case 'idle':
this.elements.calcButton.disabled = false;
this.elements.cancelButton.disabled = true;
this.elements.progressContainer.style.display =
'none';
break;
}
}
cleanupWorker() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
handleError(error) {
alert(`エラーが発生しました: ${error.message}`);
this.isCalculating = false;
this.updateUI('idle');
this.cleanupWorker();
}
}
// アプリケーションの初期化
document.addEventListener('DOMContentLoaded', () => {
new PrimeCalculator();
});
ファイル処理の実装
次に、CSV ファイルの解析処理を Web Workers で実装してみましょう。大量のデータを扱う場合に威力を発揮します。
CSV パーサー Worker(csv-worker.js)
javascript// csv-worker.js
self.addEventListener('message', function (event) {
const { type, data } = event.data;
switch (type) {
case 'PARSE_CSV':
parseCSV(data.csvText, data.options);
break;
}
});
function parseCSV(csvText, options = {}) {
const {
delimiter = ',',
hasHeader = true,
chunkSize = 1000,
} = options;
const lines = csvText.split('\n');
const totalLines = lines.length;
let processedLines = 0;
const result = {
headers: [],
data: [],
summary: {
totalRows: 0,
totalColumns: 0,
errors: [],
},
};
try {
// ヘッダー処理
if (hasHeader && lines.length > 0) {
result.headers = parseCSVLine(lines[0], delimiter);
processedLines = 1;
}
// データ行の処理
for (let i = processedLines; i < lines.length; i++) {
const line = lines[i].trim();
if (line === '') continue; // 空行をスキップ
try {
const rowData = parseCSVLine(line, delimiter);
result.data.push(rowData);
// 進捗報告
if (i % chunkSize === 0) {
const progress = (i / totalLines) * 100;
self.postMessage({
type: 'PROGRESS',
data: {
progress: progress,
processedRows: result.data.length,
currentLine: i,
},
});
}
} catch (error) {
result.summary.errors.push({
line: i + 1,
error: error.message,
content: line,
});
}
}
// サマリー情報の設定
result.summary.totalRows = result.data.length;
result.summary.totalColumns =
result.headers.length ||
(result.data.length > 0 ? result.data[0].length : 0);
// 完了通知
self.postMessage({
type: 'PARSE_COMPLETE',
data: result,
});
} catch (error) {
self.postMessage({
type: 'PARSE_ERROR',
data: { error: error.message },
});
}
}
function parseCSVLine(line, delimiter) {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
// エスケープされた引用符
current += '"';
i++; // 次の文字をスキップ
} else {
// 引用符の開始または終了
inQuotes = !inQuotes;
}
} else if (char === delimiter && !inQuotes) {
// 区切り文字(引用符外)
result.push(current.trim());
current = '';
} else {
current += char;
}
}
// 最後のフィールドを追加
result.push(current.trim());
return result;
}
CSV パーサーのメインロジック(csv-parser.js)
javascript// csv-parser.js
class CSVParser {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.worker = null;
this.setupUI();
}
setupUI() {
this.container.innerHTML = `
<div class="csv-parser">
<h3>CSV Parser Demo</h3>
<div class="upload-area">
<input type="file" id="csv-file" accept=".csv,.txt">
<label for="csv-file">CSVファイルを選択</label>
</div>
<div class="options">
<label>
<input type="checkbox" id="has-header" checked>
ヘッダー行あり
</label>
<label>
区切り文字:
<select id="delimiter">
<option value=",">カンマ (,)</option>
<option value="\t">タブ</option>
<option value=";">セミコロン (;)</option>
</select>
</label>
</div>
<button id="parse-button" disabled>解析開始</button>
<div id="progress-area" style="display: none;">
<div>進捗: <span id="progress-text">0%</span></div>
<progress id="progress-bar" value="0" max="100"></progress>
</div>
<div id="result-area" style="display: none;">
<h4>解析結果</h4>
<div id="summary"></div>
<div id="data-preview"></div>
<div id="errors" style="display: none;"></div>
</div>
</div>
`;
this.setupEventListeners();
}
setupEventListeners() {
const fileInput =
this.container.querySelector('#csv-file');
const parseButton =
this.container.querySelector('#parse-button');
fileInput.addEventListener('change', (event) => {
parseButton.disabled = !event.target.files[0];
});
parseButton.addEventListener('click', () => {
this.parseFile();
});
}
async parseFile() {
const fileInput =
this.container.querySelector('#csv-file');
const hasHeaderCheckbox =
this.container.querySelector('#has-header');
const delimiterSelect =
this.container.querySelector('#delimiter');
const file = fileInput.files[0];
if (!file) return;
try {
const csvText = await this.readFile(file);
this.showProgress();
// Workerを作成して処理開始
this.worker = new Worker('./csv-worker.js');
this.setupWorkerListeners();
this.worker.postMessage({
type: 'PARSE_CSV',
data: {
csvText: csvText,
options: {
hasHeader: hasHeaderCheckbox.checked,
delimiter: delimiterSelect.value,
},
},
});
} catch (error) {
alert(`ファイル読み込みエラー: ${error.message}`);
}
}
readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
});
}
setupWorkerListeners() {
this.worker.addEventListener('message', (event) => {
const { type, data } = event.data;
switch (type) {
case 'PROGRESS':
this.updateProgress(data);
break;
case 'PARSE_COMPLETE':
this.handleParseComplete(data);
break;
case 'PARSE_ERROR':
this.handleParseError(data);
break;
}
});
}
updateProgress(data) {
const progressText = this.container.querySelector(
'#progress-text'
);
const progressBar =
this.container.querySelector('#progress-bar');
progressText.textContent = `${data.progress.toFixed(
1
)}% (${data.processedRows}行処理済み)`;
progressBar.value = data.progress;
}
handleParseComplete(data) {
this.hideProgress();
this.showResults(data);
this.cleanupWorker();
}
handleParseError(data) {
alert(`解析エラー: ${data.error}`);
this.hideProgress();
this.cleanupWorker();
}
showProgress() {
this.container.querySelector(
'#progress-area'
).style.display = 'block';
this.container.querySelector(
'#result-area'
).style.display = 'none';
}
hideProgress() {
this.container.querySelector(
'#progress-area'
).style.display = 'none';
}
showResults(data) {
const resultArea =
this.container.querySelector('#result-area');
const summaryDiv =
this.container.querySelector('#summary');
const previewDiv =
this.container.querySelector('#data-preview');
const errorsDiv =
this.container.querySelector('#errors');
// サマリー表示
summaryDiv.innerHTML = `
<p><strong>総行数:</strong> ${data.summary.totalRows.toLocaleString()}</p>
<p><strong>カラム数:</strong> ${
data.summary.totalColumns
}</p>
<p><strong>エラー数:</strong> ${
data.summary.errors.length
}</p>
`;
// データプレビュー表示(最初の10行)
if (data.data.length > 0) {
const previewData = data.data.slice(0, 10);
previewDiv.innerHTML = this.createTable(
data.headers,
previewData
);
}
// エラー表示
if (data.summary.errors.length > 0) {
errorsDiv.style.display = 'block';
errorsDiv.innerHTML = `
<h5>エラー詳細</h5>
<ul>
${data.summary.errors
.map(
(error) =>
`<li>行 ${error.line}: ${error.error}</li>`
)
.join('')}
</ul>
`;
}
resultArea.style.display = 'block';
}
createTable(headers, data) {
let html = '<table border="1"><thead><tr>';
if (headers.length > 0) {
headers.forEach((header) => {
html += `<th>${this.escapeHtml(header)}</th>`;
});
} else if (data.length > 0) {
data[0].forEach((_, index) => {
html += `<th>Column ${index + 1}</th>`;
});
}
html += '</tr></thead><tbody>';
data.forEach((row) => {
html += '<tr>';
row.forEach((cell) => {
html += `<td>${this.escapeHtml(cell)}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
return html;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
cleanupWorker() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}
// 使用例
document.addEventListener('DOMContentLoaded', () => {
new CSVParser('csv-container');
});
データ変換処理
最後に、JSON 形式のデータを効率的に変換する例を見てみましょう。
データ変換 Worker(transform-worker.js)
javascript// transform-worker.js
self.addEventListener('message', function (event) {
const { type, data } = event.data;
switch (type) {
case 'TRANSFORM_DATA':
transformData(
data.items,
data.transformType,
data.options
);
break;
}
});
function transformData(items, transformType, options = {}) {
const batchSize = options.batchSize || 1000;
const result = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
let transformedItem;
try {
switch (transformType) {
case 'NORMALIZE':
transformedItem = normalizeItem(item, options);
break;
case 'AGGREGATE':
transformedItem = aggregateItem(item, options);
break;
case 'FILTER':
if (filterItem(item, options)) {
transformedItem = item;
} else {
continue; // フィルターで除外
}
break;
default:
transformedItem = item;
}
result.push(transformedItem);
// 進捗報告
if (i % batchSize === 0) {
const progress = (i / items.length) * 100;
self.postMessage({
type: 'TRANSFORM_PROGRESS',
data: {
progress: progress,
processed: i,
total: items.length,
resultCount: result.length,
},
});
}
} catch (error) {
self.postMessage({
type: 'TRANSFORM_ERROR',
data: {
index: i,
item: item,
error: error.message,
},
});
}
}
// 完了通知
self.postMessage({
type: 'TRANSFORM_COMPLETE',
data: {
result: result,
originalCount: items.length,
transformedCount: result.length,
},
});
}
function normalizeItem(item, options) {
const normalized = { ...item };
// 数値の正規化
if (
options.normalizeNumbers &&
typeof item.value === 'number'
) {
normalized.value =
(item.value - options.min) /
(options.max - options.min);
}
// 文字列の正規化
if (
options.normalizeStrings &&
typeof item.name === 'string'
) {
normalized.name = item.name.toLowerCase().trim();
}
// 日付の正規化
if (options.normalizeDates && item.date) {
normalized.date = new Date(item.date).toISOString();
}
return normalized;
}
function aggregateItem(item, options) {
// グループ化のキーを生成
const groupKey = options.groupBy
.map((field) => item[field])
.join('|');
return {
groupKey: groupKey,
...item,
aggregatedAt: new Date().toISOString(),
};
}
function filterItem(item, options) {
for (const condition of options.conditions) {
const { field, operator, value } = condition;
const itemValue = item[field];
switch (operator) {
case 'equals':
if (itemValue !== value) return false;
break;
case 'greater':
if (itemValue <= value) return false;
break;
case 'less':
if (itemValue >= value) return false;
break;
case 'contains':
if (!String(itemValue).includes(value))
return false;
break;
default:
return false;
}
}
return true;
}
エラーハンドリング
Web Workers を使用する際の適切なエラーハンドリングパターンを実装しましょう。
javascript// error-handling.js
class RobustWorkerManager {
constructor(workerPath) {
this.workerPath = workerPath;
this.worker = null;
this.retryCount = 0;
this.maxRetries = 3;
this.timeout = 30000; // 30秒のタイムアウト
this.timeoutId = null;
}
async executeTask(taskData) {
return new Promise((resolve, reject) => {
this.createWorker();
// タイムアウト設定
this.timeoutId = setTimeout(() => {
this.handleTimeout();
reject(new Error('Worker task timeout'));
}, this.timeout);
// Worker完了時の処理
const handleComplete = (result) => {
this.clearTimeout();
this.cleanup();
resolve(result);
};
// Workerエラー時の処理
const handleError = (error) => {
this.clearTimeout();
this.cleanup();
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.warn(
`Worker error, retrying (${this.retryCount}/${this.maxRetries}):`,
error
);
// 少し待ってからリトライ
setTimeout(() => {
this.executeTask(taskData)
.then(resolve)
.catch(reject);
}, 1000 * this.retryCount);
} else {
reject(error);
}
};
// イベントリスナー設定
this.worker.addEventListener('message', (event) => {
const { type, data } = event.data;
if (type === 'TASK_COMPLETE') {
handleComplete(data);
} else if (type === 'TASK_ERROR') {
handleError(new Error(data.message));
}
});
this.worker.addEventListener('error', (error) => {
handleError(error);
});
// タスク開始
this.worker.postMessage({
type: 'EXECUTE_TASK',
data: taskData,
});
});
}
createWorker() {
if (this.worker) {
this.worker.terminate();
}
try {
this.worker = new Worker(this.workerPath);
} catch (error) {
throw new Error(
`Failed to create worker: ${error.message}`
);
}
}
handleTimeout() {
console.error('Worker task timeout');
this.cleanup();
}
clearTimeout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
cleanup() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.clearTimeout();
}
// リトライカウントをリセット
resetRetryCount() {
this.retryCount = 0;
}
}
// 使用例
async function demonstrateErrorHandling() {
const workerManager = new RobustWorkerManager(
'./task-worker.js'
);
try {
const result = await workerManager.executeTask({
operation: 'HEAVY_CALCULATION',
parameters: { iterations: 1000000 },
});
console.log('Task completed successfully:', result);
workerManager.resetRetryCount();
} catch (error) {
console.error('Task failed after retries:', error);
// フォールバック処理
performFallbackOperation();
}
}
function performFallbackOperation() {
console.log(
'Performing fallback operation on main thread...'
);
// メインスレッドでの代替処理
}
これらの具体例を通して、Web Workers の実践的な活用方法を学んでいただけたでしょうか。次のセクションでは、これまでの内容をまとめていきます。
まとめ
Web Workers 活用のメリット
本記事を通じて、Web Workers の導入により以下のような大きなメリットが得られることをご理解いただけたと思います。
メリット | 具体的な効果 | ユーザー体験への影響 |
---|---|---|
UI 応答性の向上 | 重い処理中も UI が操作可能 | ストレスフリーな操作感 |
処理性能の最適化 | 並列処理による高速化 | 待ち時間の短縮 |
ユーザビリティ改善 | 進捗表示とキャンセル機能 | 安心感と操作性向上 |
スケーラビリティ | 大量データ処理が現実的 | 機能の拡張可能性 |
使用上の注意点
Web Workers を効果的に活用するために、以下の点にご注意ください。
パフォーマンスに関する注意
- Worker 作成コスト:頻繁な作成は避け、可能な限り再利用しましょう
- メッセージ通信オーバーヘッド:大量の小さなメッセージより、少数の大きなメッセージが効率的です
- メモリ使用量:Worker は独立したコンテキストを持つため、メモリ使用量が増加します
設計上の考慮点
javascript// 良い例:効率的なWorker利用
class EfficientWorkerPool {
constructor(workerPath, poolSize = 4) {
this.workers = [];
this.taskQueue = [];
this.activeWorkers = new Set();
// Workerプールを事前に作成
for (let i = 0; i < poolSize; i++) {
this.workers.push(new Worker(workerPath));
}
}
async executeTask(taskData) {
return new Promise((resolve, reject) => {
const availableWorker = this.getAvailableWorker();
if (availableWorker) {
this.runTask(
availableWorker,
taskData,
resolve,
reject
);
} else {
// キューに追加
this.taskQueue.push({ taskData, resolve, reject });
}
});
}
getAvailableWorker() {
return this.workers.find(
(worker) => !this.activeWorkers.has(worker)
);
}
runTask(worker, taskData, resolve, reject) {
this.activeWorkers.add(worker);
const cleanup = () => {
this.activeWorkers.delete(worker);
this.processQueue(); // キューの次のタスクを実行
};
worker.addEventListener(
'message',
(event) => {
if (event.data.type === 'TASK_COMPLETE') {
resolve(event.data.result);
cleanup();
}
},
{ once: true }
);
worker.addEventListener(
'error',
(error) => {
reject(error);
cleanup();
},
{ once: true }
);
worker.postMessage(taskData);
}
processQueue() {
if (this.taskQueue.length > 0) {
const availableWorker = this.getAvailableWorker();
if (availableWorker) {
const { taskData, resolve, reject } =
this.taskQueue.shift();
this.runTask(
availableWorker,
taskData,
resolve,
reject
);
}
}
}
}
ブラウザ対応とフォールバック
javascript// ブラウザサポートチェックとフォールバック
function createWorkerWithFallback(
workerPath,
fallbackFunction
) {
if (typeof Worker !== 'undefined') {
try {
return new Worker(workerPath);
} catch (error) {
console.warn(
'Worker creation failed, using fallback:',
error
);
return createFallbackWorker(fallbackFunction);
}
} else {
console.warn(
'Web Workers not supported, using fallback'
);
return createFallbackWorker(fallbackFunction);
}
}
function createFallbackWorker(fallbackFunction) {
return {
postMessage: (data) => {
// メインスレッドで非同期実行
setTimeout(() => {
try {
const result = fallbackFunction(data);
this.onmessage({
data: { type: 'COMPLETE', result },
});
} catch (error) {
this.onerror(error);
}
}, 0);
},
terminate: () => {
// フォールバック用のクリーンアップ
},
onmessage: null,
onerror: null,
};
}
次のステップ
Web Workers の基本を習得した後は、以下のような発展的なトピックに取り組まれることをお勧めします。
1. SharedArrayBuffer の活用
より高度なデータ共有が必要な場合は、SharedArrayBuffer を検討してみてください。
2. OffscreenCanvas との組み合わせ
グラフィック処理を Worker で行う場合は、OffscreenCanvas が強力な選択肢となります。
3. WebAssembly (WASM) との統合
計算集約的な処理では、WebAssembly と Web Workers を組み合わせることで、さらなる性能向上が期待できます。
4. Service Workers との使い分け
Service Workers と Web Workers の使い分けを理解し、適切な場面で活用しましょう。
Web Workers は、モダン Web アプリケーションにとって欠かせない技術です。ユーザー体験を大幅に改善できる強力なツールですので、ぜひ積極的に活用してみてください。実際のプロジェクトで使用する際は、小さな機能から始めて、徐々に適用範囲を広げていくことをお勧めいたします。
関連リンク
- article
JavaScript Web Workers 実践入門:重い処理を別スレッドへ逃がす最短手順
- article
JavaScript OffscreenCanvas 検証:Canvas/OffscreenCanvas/WebGL の速度比較
- article
JavaScript メモリリーク診断術:DevTools の Heap スナップショット徹底活用
- article
JavaScript Streams API 活用ガイド:巨大データを分割して途切れず処理する
- article
【早見表】JavaScript でよく使う Math メソッドの一覧と活用事例
- article
JavaScript のオブジェクト操作まとめ:Object.keys/entries/values の使い方
- article
【保存版】Vite 設定オプション早見表:`resolve` / `optimizeDeps` / `build` / `server`
- article
JavaScript Web Workers 実践入門:重い処理を別スレッドへ逃がす最短手順
- article
htmx × Express/Node.js 高速セットアップ:テンプレ・部分テンプレ構成の定石
- article
TypeScript 型縮小(narrowing)パターン早見表:`in`/`instanceof`/`is`/`asserts`完全対応
- article
Homebrew を社内プロキシで使う設定完全ガイド:HTTP(S)_PROXY・証明書・ミラー最適化
- article
Tauri 開発環境の最速構築:Node・Rust・WebView ランタイムの完全セットアップ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来