はじめに
この記事を読めば、JavaScriptでマルチスレッドを活用することができるようになります。
JavaScriptのマルチスレッド処理は、Web Workersを使って実現されます。
この記事では、Web Workersの基本から使い方、注意点、カスタマイズ方法まで、初心者向けに徹底解説します。
また、実践的なサンプルコードも10個紹介するので、ぜひ参考にしてみてください。
●JavaScriptでのマルチスレッドとは
JavaScriptはもともとシングルスレッドで動作する言語ですが、Web Workersを使うことでマルチスレッド処理を実現することができます。
マルチスレッド処理により、複数のタスクを並行して処理することが可能となり、パフォーマンスの向上が期待できます。
○Web Workersの基本
Web Workersは、JavaScriptのメインスレッドとは別にバックグラウンドで動作するスレッドを生成することができる機能です。
これにより、重たい処理をバックグラウンドで実行させることで、UIのフリーズを防ぐことができます。
●マルチスレッドの使い方
では、実際にWeb Workersを使用してマルチスレッド処理を実現する方法を見ていきましょう。
下記のサンプルコードでは、単純な計算や画像処理、Ajaxリクエストを並列処理する例を紹介します。
○サンプルコード1:単純な計算を並列処理
まずは、単純な計算を並列処理する例を見ていきましょう。
下記のサンプルコードでは、1から10000までの数字を足し合わせる処理を行っています。
【メインスレッド側のコード】
// worker.jsを読み込む
const worker = new Worker("worker.js");
// メッセージを受け取った時の処理
worker.addEventListener("message", (event) => {
console.log("Result:", event.data);
});
// 処理を開始するメッセージを送信
worker.postMessage({ start: 1, end: 10000 });
【worker.js(Web Worker側のコード)】
// メッセージを受け取った時の処理
self.addEventListener("message", (event) => {
const { start, end } = event.data;
let sum = 0;
// 1から10000までの数字を足し合わせる
for (let i = start; i <= end; i++) {
sum += i;
}
// 結果をメインスレッドに送信
self.postMessage(sum);
});
○サンプルコード2:画像処理を並列処理
次に、画像処理を並列処理する例を見ていきます。
下記のサンプルコードでは、画像のグレースケール変換を行っています。
【メインスレッド側のコード】
const img = new Image();
img.src = "path/to/image.jpg";
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// worker.jsを読み込む
const worker = new Worker("worker.js");
// メッセージを受け取った時の処理
worker.addEventListener("message", (event) => {
// グレースケール変換後の画像データを受け取る
const grayScaleImageData = event.data;
ctx.putImageData(grayScaleImageData, 0, 0);
});
// 画像データをWeb Workerに送信
worker.postMessage(imageData);
};
【worker.js(Web Worker側のコード)】
// メッセージを受け取った時の処理
self.addEventListener("message", (event) => {
const imageData = event.data;
const { data, width, height } = imageData;
// グレースケール変換を行う
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
// 結果をメインスレッドに送信
self.postMessage(imageData);
});
○サンプルコード3:Ajaxリクエストを並列処理
最後に、Ajaxリクエストを並列処理する例を見ていきます。
下記のサンプルコードでは、複数のURLからデータを取得する処理を並行して行っています。
【メインスレッド側のコード】
const urls = ["url1", "url2", "url3"];
// worker.jsを読み込む
const worker = new Worker("worker.js");
// メッセージを受け取った時の処理
worker.addEventListener("message", (event) => {
const { url, data } = event.data;
console.log(`Data from ${url}:`, data);
});
// URLをWeb Workerに送信
urls.forEach((url) => {
worker.postMessage({ url });
});
【worker.js(Web Worker側のコード)】
// メッセージを受け取った時の処理
self.addEventListener("message", async (event) => {
const { url } = event.data;
// Ajaxリクエストを行う
const response = await fetch(url);
const data = await response.json();
// 結果をメインスレッドに送信
self.postMessage({ url, data });
});
これで、Ajaxリクエストを並列処理する方法が分かりました。
次に、注意点と対処法について説明します。
●注意点と対処法
- DOM操作はWeb Worker内で行えません。DOM操作はメインスレッド側で行う必要があります。
- メインスレッドとWeb Worker間でデータのやり取りをする際には、データのコピーが発生します。
データ量が大きい場合、パフォーマンスに影響が出る可能性があります。
SharedArrayBufferやTransferable Objectsを使用してデータのコピーを抑える方法があります。
次に、カスタマイズ方法について見ていきます。
●カスタマイズ方法
- データ送受信のカスタマイズ
- エラーハンドリングのカスタマイズ
それぞれのカスタマイズ方法について、サンプルコードを用意しました。
○サンプルコード4:データ送受信のカスタマイズ
下記のサンプルコードでは、Transferable Objectsを使って、データをコピーせずに効率的に送受信する方法を表しています。
【メインスレッド側のコード】
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// データをセット
view.setInt32(0, 100);
view.setInt32(4, 200);
const worker = new Worker("worker.js");
worker.addEventListener("message", (event) => {
const { buffer } = event.data;
const view = new DataView(buffer);
// 受信したデータを表示
console.log(view.getInt32(0), view.getInt32(4));
});
// データをWeb Workerに送信(Transferable Objectsを使用)
worker.postMessage({ buffer }, [buffer]);
【worker.js(Web Worker側のコード)】
self.addEventListener("message", (event) => {
const { buffer } = event.data;
const view = new DataView(buffer);
// データを加工
view.setInt32(0, view.getInt32(0) * 2);
view.setInt32(4, view.getInt32(4) * 2);
// データをメインスレッドに送信(Transferable Objectsを使用)
self.postMessage({ buffer }, [buffer]);
});
○サンプルコード5:エラーハンドリングのカスタマイズ
下記のサンプルコードでは、Web Worker内でエラーが発生した場合のエラーハンドリングをカスタマイズする方法を表しています。
【メインスレッド側のコード】
const worker = new Worker("worker.js");
// エラーが発生した場合の処理
worker.addEventListener("error", (event) => {
console.error("Web Worker error:", event.message);
});
worker.postMessage("Hello, Worker!");
【worker.js(Web Worker側のコード)】
self.addEventListener("message", (event) => {
// わざとエラーを発生させる
throw new Error("エラーが発生しました");
});
これで、データ送受信のカスタマイズとエラーハンドリングのカスタマイズの方法が分かりました。
●応用例とサンプルコード
次に、応用例とサンプルコードを見ていきましょう。
○サンプルコード6:複数のWeb Workersを組み合わせる
下記のサンプルコードでは、2つのWeb Workersを組み合わせて、異なる処理を同時に実行する方法を表しています。
【メインスレッド側のコード】
// Web Worker 1
const worker1 = new Worker("worker1.js");
worker1.addEventListener("message", (event) => {
console.log("Worker 1の結果: ", event.data);
});
worker1.postMessage("Worker 1へのデータ");
// Web Worker 2
const worker2 = new Worker("worker2.js");
worker2.addEventListener("message", (event) => {
console.log("Worker 2の結果: ", event.data);
});
worker2.postMessage("Worker 2へのデータ");
【worker1.js(Web Worker 1のコード)】
self.addEventListener("message", (event) => {
const data = event.data;
// ここで何らかの処理を行う
const result = `Worker 1が受け取ったデータ: ${data}`;
self.postMessage(result);
});
【worker2.js(Web Worker 2のコード)】
self.addEventListener("message", (event) => {
const data = event.data;
// ここで何らかの処理を行う
const result = `Worker 2が受け取ったデータ: ${data}`;
self.postMessage(result);
});
○サンプルコード7:Web WorkersとPromiseを組み合わせる
下記のサンプルコードでは、Web WorkersとPromiseを組み合わせて、非同期処理を扱いやすくする方法を表しています。
【メインスレッド側のコード】
function runWorker(workerUrl, data) {
return new Promise((resolve, reject) => {
const worker = new Worker(workerUrl);
worker.addEventListener("message", (event) => {
resolve(event.data);
});
worker.addEventListener("error", (event) => {
reject(event.message);
});
worker.postMessage(data);
});
}
(async () => {
try {
const result = await runWorker("worker.js", "データを送信");
console.log("結果: ", result);
} catch (error) {
console.error("エラー: ", error);
}
})();
【worker.js(Web Worker側のコード)】
self.addEventListener("message", (event) => {
const data = event.data;
// ここで何らかの処理を行う
const result = `受け取ったデータ: ${data}`;
self.postMessage(result);
});
○サンプルコード8:Web WorkersとSharedArrayBufferを使った高速処理
下記のサンプルコードでは、Web WorkersとSharedArrayBufferを使って、高速なデータ処理を行う方法を表しています。
【メインスレッド側のコード】
// バッファのサイズを定義
const bufferSize = 10;
// SharedArrayBufferを作成
const sharedArrayBuffer = new SharedArrayBuffer(bufferSize * Int32Array.BYTES_PER_ELEMENT);
// Int32Arrayを作成
const intArray = new Int32Array(sharedArrayBuffer);
// Web Workerを作成
const worker = new Worker("worker.js");
// メッセージ受信時の処理
worker.addEventListener("message", (event) => {
console.log("メインスレッドが受け取ったデータ: ", intArray);
});
// Web WorkerにSharedArrayBufferを送信
worker.postMessage({ sharedArrayBuffer });
// データをセット
intArray[0] = 1;
intArray[1] = 2;
【worker.js(Web Worker側のコード)】
self.addEventListener("message", (event) => {
const { sharedArrayBuffer } = event.data;
// SharedArrayBufferからInt32Arrayを作成
const intArray = new Int32Array(sharedArrayBuffer);
// ここで何らかの処理を行う
intArray[2] = intArray[0] + intArray[1];
self.postMessage("処理が完了しました");
});
○サンプルコード9:Web WorkersとOffscreenCanvasを組み合わせる
下記のサンプルコードでは、Web WorkersとOffscreenCanvasを使って、バックグラウンドでCanvasの描画処理を行う方法を表しています。
【HTMLファイル】
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Web WorkersとOffscreenCanvasのサンプル</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script src="main.js"></script>
</body>
</html>
【main.js(メインスレッド側のコード)】
const canvas = document.getElementById("canvas");
const offscreenCanvas = canvas.transferControlToOffscreen();
const worker = new Worker("worker.js");
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);
【worker.js(Web Worker側のコード)】
self.addEventListener("message", (event) => {
const { offscreenCanvas } = event.data;
// 描画コンテキストを取得
const ctx = offscreenCanvas.getContext("2d");
// ここで何らかの描画処理を行う
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 100);
});
○サンプルコード10:Service Workersでのマルチスレッド処理
下記のサンプルコードでは、Service Workersを使用してマルチスレッド処理を行う方法を表しています。
この例では、キャッシュを利用してオフラインでもアクセス可能なWebアプリケーションを構築しています。
【HTMLファイル】
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Service Workersのサンプル</title>
</head>
<body>
<h1>Service Workersでのマルチスレッド処理</h1>
<script src="main.js"></script>
</body>
</html>
【main.js(メインスレッド側のコード)】
// サービスワーカーを登録
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("service-worker.js")
.then(() => {
console.log("サービスワーカーが登録されました");
})
.catch((error) => {
console.error("サービスワーカーの登録に失敗しました:", error);
});
}
【service-worker.js(Service Worker側のコード)】
// キャッシュ名とキャッシュ対象のファイルを定義
const CACHE_NAME = "my-cache";
const CACHE_FILES = ["index.html", "main.js"];
// インストール時のキャッシュ処理
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log("キャッシュが開かれました");
return cache.addAll(CACHE_FILES);
})
);
});
// フェッチ時のキャッシュ処理
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// キャッシュがあればそれを返す、なければネットワークから取得
return response || fetch(event.request);
})
);
});
このサンプルコードを使用することで、Service Workersを利用したマルチスレッド処理を実現することができます。
これにより、Webアプリケーションのパフォーマンス向上やオフライン対応が可能になります。
まとめ
Web WorkersやService Workersを使って、マルチスレッド処理を行うことで、Webアプリケーションのパフォーマンスを向上させることができます。
また、PromiseやSharedArrayBuffer、OffscreenCanvasなどの技術を組み合わせることで、さらに効率的な処理が可能になります。
これらのサンプルコードを参考に、Webアプリケーションの開発が向上すれば嬉しい限りです。