こんにちは!JavaScriptのコアな仕組みへようこそ。
普段私たちが書いているJavaScriptは、基本的に「シングルスレッド(=同時に1つのことしか処理できない)」の世界で動いています。だからこそ、重い処理をさせると画面がカクついたり、ボタンが反応しなくなったりしてしまいますよね。
そこで登場するのが「Web Worker」です。Web Workerを使えば、メインの画面描画スレッドとは完全に独立した「裏側の作業部屋」で重い計算やデータ処理を行わせることができます。
今回は、このWeb Workerとメインスレッドの間で、変数をどのように受け渡し、どうやってメモリを共有するのかという、一歩進んだスコープとメモリ管理の世界を一緒に紐解いていきましょう。ここをクリアすれば、ブラウザのパフォーマンスを極限まで引き出すアーキテクチャの基本がバッチリマスターできますよ!
—
1. なぜWeb Workerでは「変数の共有」が難しいのか?
JavaScriptの通常の関数やスコープなら、外側の変数(クロージャなど)をそのまま参照したり書き換えたりできますよね。しかし、Web Workerの世界は、メインスレッドとは「完全に隔離されたメモリ空間」になっています。
イメージとしては、こういう状態です。
[ メインスレッド ] (画面描画・ユーザー操作)
│
│ ← 共有メモリの壁 🧱 →
│
[ Web Worker ] (重い計算処理・裏方)
お互いのメモリ領域(ヒープ)に直接アクセスすることはセキュリティ上もパフォーマンス上も許されていません。では、どうやってデータをやり取りするのでしょうか?
答えは「メッセージの送受信(通信)」です。手紙を書いて相手に渡し、相手がそれを読み取って自分の手元でコピーを作る、というイメージですね。
—
2. 構造化クローン(Structured Clone Algorithm)によるデータの受け渡し
Web Workerへデータを送るとき、ブラウザは内部で「Structured Clone Algorithm(構造化クローンアルゴリズム)」という仕組みを使っています。
これは、JSONのシリアライズ(`JSON.stringify`など)とは違って、`Date`オブジェクトや`RegExp`、さらには循環参照を含むオブジェクトまでディープコピー(値の完全な複製)して送信できる優れものです。
実際のコードを見てみましょう。
メインスレッド側のコード (`main.js`)
// 新しいWeb Worker(裏方の作業部屋)を生成します
const worker = new Worker(‘worker.js’);
// 労働者(Worker)へ送るデータ
const complexData = {
task: ‘heavy-computation’,
payload: [1, 2, 3, 4, 5],
metadata: { timestamp: new Date() }
};
// postMessageでデータを「コピーして」送信します
// ※この瞬間、complexDataのディープコピーが作られ、Workerへ旅立ちます
worker.postMessage(complexData);
// Workerからの返信を受け取る
worker.onmessage = function(event) {
console.log(‘メインスレッド: Workerから結果を受け取りました’, event.data);
};
Worker側のコード (`worker.js`)
// メインスレッドからメッセージが届いたときのイベント
onmessage = function(event) {
// event.dataには、メインスレッドから送られたデータの「コピー」が入っています
const receivedData = event.data;
console.log(‘Worker: データを受信しました’, receivedData);
// 何かしらの重い計算処理をする(例:配列を2倍にする)
const result = receivedData.payload.map(num => num 2);
// 計算結果をメインスレッドに送り返す(これもコピーされます)
postMessage({ status: ‘success’, result: result });
};
💡 ここでの注意点(パフォーマンスの罠)
`postMessage`はとても便利ですが、データを「コピー」して渡すため、もし数ギガバイトもあるような巨大なデータをやり取りすると、コピーの作成コスト(メモリ消費とCPU時間)でメインスレッドがフリーズしてしまいます。
「コピーしたくない!巨大なメモリをそのまま効率よく共有したい!」
そんな上級者の悩みを解決するのが、次に紹介する `SharedArrayBuffer` です。
—
3. SharedArrayBuffer と型付き配列による真のメモリ共有
「コピーするのではなく、同じメモリ領域を両方のスレッドから直接読み書きしたい!」
それを実現するのが `SharedArrayBuffer`(共有配列バッファ) です。
これを使うと、メインスレッドとWeb Workerが文字通り「同じメモリの番地」を指し示すことができます。
[ メインスレッド ] ──┐
▼
[ SharedArrayBuffer ] (共有メモリ空間)
▲
[ Web Worker ] ──┘
実装例:SharedArrayBufferを使ったデータ共有
メインスレッド側
// 4バイト(32ビット整数1つ分)の共有メモリ領域を作成します
const sharedBuffer = new SharedArrayBuffer(4);
// 32ビット符号付き整数として扱えるようにビュー(型付き配列)を作ります
const sharedArray = new Int32Array(sharedBuffer);
// 初期値を設定してみましょう
sharedArray[0] = 42;
// Workerにこのバッファを渡します
const worker = new Worker(‘worker.js’);
worker.postMessage(sharedBuffer);
// 少し待ってから、Worker側で書き換えられた値を確認してみます
setTimeout(() => {
console.log(‘メインスレッド: Workerが書き換えた値 →’, sharedArray[0]);
// 出力例: 100 (Worker側で書き換えた内容が即座に見える!)
}, 1000);
Worker側 (`worker.js`)
onmessage = function(event) {
// メインスレッドから共有バッファを受け取る
const sharedBuffer = event.data;
const sharedArray = new Int32Array(sharedBuffer);
// メモリ上の値を直接書き換える!
console.log(‘Worker: 受け取った共有メモリの値:’, sharedArray[0]);
// 別の値に書き換える
sharedArray[0] = 100;
console.log(‘Worker: メモリの値を 100 に書き換えました’);
};
—
4. 陥りやすい罠とマルチスレッドの落とし穴:データ競合(Data Race)
`SharedArrayBuffer`は非常に強力ですが、ここでマルチスレッド特有の恐ろしいバグに直面するリスクが生まれます。それが「データ競合(Data Race)」です。
メインスレッドとWorkerが、お互いのタイミングを気にせず「同じメモリの同じ場所」を同時に書き換えようとすると、一方が書き込んだ内容がもう一方によって上書きされて消えてしまったり、中途半端なデータが読み込まれたりする予測不能なバグが発生します。
これを防ぐために用意されているのが、`Atomics` オブジェクトです。
Atomicsによる安全な排他制御
Atomicsを使うと、メモリの読み書きを「アトミック(不可分=途中で割り込みができない単位)」に行うことができます。
// メインスレッドやWorker内で、安全に値に1を加算する例
// 第1引数: 配列, 第2引数: インデックス, 第3引数: 加算する値
Atomics.add(sharedArray, 0, 1);
// 安全に値を取り出す
const currentValue = Atomics.load(sharedArray, 0);
もし複数のスレッド間で複雑な同期を取る必要がある場合は、`Atomics.wait()` や `Atomics.notify()` を駆使して、まるでC++やJavaのマルチスレッドプログラミングのような緻密なスコープ・メモリ管理を行うことになります。
—
まとめ:今回の極限知見の整理
1. 基本は「メッセージパッシング」
- Web Workerとメインスレッドのメモリ空間は完全に別々。
- データのやり取りは基本 `postMessage` による「構造化クローン(コピー)」で行われる。
2. 巨大データには `SharedArrayBuffer`
- コピーのオーバーヘッドをなくし、メモリを直接共有したい場合は `SharedArrayBuffer` を利用する。
3. マルチスレッドの副作用に注意
- 共有メモリを同時に書き換えるリスク(データ競合)があるため、安全に操作したい場合は `Atomics` API を活用する。
Web Workerとスコープの分離、そしてメモリ共有のメカニズムを理解することで、あなたも単なる「ブラウザ向けのスクリプト書き」から、堅牢な非同期・並行処理システムを設計できる真のフルスタック・アーキテクトへとステップアップできます。
ここをクリアできれば、JavaScriptのランタイムの挙動に関する理解はもうバッチリですよ!次の開発案件でぜひ試してみてくださいね。