



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp








生徒Web WorkersでSharedArrayBufferを使おうとしたらエラーになりました。「SharedArrayBuffer is not defined」って……



Spectre脆弱性対策でSharedArrayBufferはクロスオリジン分離された環境でしか使えなくなったんじゃ。COOP(Cross-Origin-Opener-Policy)とCOEP(Cross-Origin-Embedder-Policy)という2つのHTTPヘッダーを設定すれば解決できるぞ。サーバーの設定が必要じゃが手順を覚えれば簡単なんじゃ!
SharedArrayBufferは複数のWeb Workers間でメモリを共有し、真の並列処理を実現するJavaScript APIです。しかし2018年のSpectre脆弱性発見を受けてデフォルト無効化され、現在は「クロスオリジン分離」環境でのみ使用可能です。
本記事では、SharedArrayBufferの概要・クロスオリジン分離とは何か・COOP/COEPヘッダーの設定(Next.js・Nginx・Vercel・Apache・Caddy)・Atomics APIによる競合回避・Mutexパターン・実践的な並列処理の実装・サードパーティリソースとの互換性問題を完全解説します。
SharedArrayBufferはES2017で追加されたJavaScript APIで、複数のWorkerが同一のメモリ領域を共有して読み書きできる仕組みです。通常のpostMessageがデータのコピーを作成して転送するのに対し、SharedArrayBufferはメモリの参照を共有するため、コピーコストがゼロです。
| 方式 | 仕組み | メモリ使用量 | 双方向更新 | 適したユースケース |
|---|---|---|---|---|
| postMessage(コピー) | 構造化クローンでデータをコピーして送信 | 2倍(コピー分) | 可能(毎回コピー) | 小〜中規模データの一時的な受け渡し |
| Transferable(所有権移転) | ArrayBufferの所有権を移転(コピーなし) | 変わらず | 不可(元スレッドで使えなくなる) | 大量データを一方向に渡す |
| SharedArrayBuffer(共有メモリ) | 複数のWorkerが同じメモリ空間を参照 | 変わらず | 可能(コピーなし) | Workerが同じバッファを並列読み書きする |
>【SharedArrayBufferが特に有効なユースケース】
✅ 大量データのWorker間リアルタイム共有
✅ 複数Workerで分担して1つのバッファを並列処理
✅ WebAssemblyの並列処理(Emscriptenの-s USE_PTHREADS=1)
✅ 頻繁な双方向のデータ更新(ゲームの状態同期など)
2018年1月、Spectre脆弱性(CVE-2018-3640等)が公開されました。SharedArrayBufferを使うとJavaScriptで高精度なタイマーを作成でき、CPUのサイドチャネル攻撃によりクロスオリジンのデータ(パスワード・Cookieなど)を読み取れる可能性があります。
この脆弱性への対策として、ChromeはSharedArrayBufferを完全に無効化しました(2018〜2021年)。2021年以降、「クロスオリジン分離」という新しいセキュリティモデルが導入され、これを有効にした環境でのみSharedArrayBufferが再び使えるようになりました。


>【クロスオリジン分離を有効にするための2つのヘッダー】
1. COOP (Cross-Origin-Opener-Policy)
値: "same-origin"
効果: 他オリジンのポップアップウィンドウとのブラウジングコンテキストグループを分離
→ window.opener で別オリジンにアクセスできなくなる
→ SNSログイン(OAuth)のポップアップには注意が必要
2. COEP (Cross-Origin-Embedder-Policy)
値: "require-corp"(または新しい "credentialless")
効果: クロスオリジンリソースの読み込みに CORP ヘッダーまたは CORS を要求
→ クロスオリジン画像や動画は cors または credentialless 属性が必要になる
→ 対応していないサードパーティリソースは読み込めなくなる
有効化の確認:
console.log(crossOriginIsolated) // → true なら有効
// → performance.now() の精度も上がる(Spectreの攻撃面を狭めるため制限されていたものが解除)
重要:この2つのヘッダーを両方設定することで crossOriginIsolated = true になる
>// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async headers() {
return [
{
// 全ページにヘッダーを適用
source: '/(.*)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Cross-Origin-Embedder-Policy',
// credentialless は Google Fonts等の一部の外部リソースと互換性がある
// require-corp は最も厳格(外部リソースにCORPヘッダーが必要)
value: 'credentialless',
},
],
},
]
},
}
export default nextConfig
># /etc/nginx/conf.d/default.conf
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# COOP/COEP ヘッダーを追加
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;
# WebAssembly ファイルのMIMEタイプ設定(Wasmを使う場合に必要)
types {
application/wasm wasm;
}
# スタティックファイル配信
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# APIリバースプロキシ(オリジンサーバーへの転送)
location /api/ {
proxy_pass http://backend:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
>{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
]
}
]
}
># .htaccess
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "credentialless"
# MIME タイプの設定(.wasm ファイルを使う場合)
AddType application/wasm .wasm
># Caddyfile
example.com {
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "credentialless"
file_server
try_files {path} /index.html
}
>// main.js - 共有バッファをWorkerに渡す
// COOP/COEP が有効かチェック
if (!crossOriginIsolated) {
throw new Error(
'クロスオリジン分離が有効になっていません。\n' +
'次のHTTPヘッダーを設定してください:\n' +
' Cross-Origin-Opener-Policy: same-origin\n' +
' Cross-Origin-Embedder-Policy: credentialless (または require-corp)'
)
}
// 4 * Int32Array.BYTES_PER_ELEMENT = 16バイトの共有バッファを作成
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT)
const sharedArray = new Int32Array(sharedBuffer)
// 初期値を設定
sharedArray[0] = 0 // カウンター
sharedArray[1] = 0 // フラグ
sharedArray[2] = 0 // ステータス
const worker = new Worker('./worker.js', { type: 'module' })
// Workerと共有バッファを共有(コピーではなく参照)
worker.postMessage({ sharedBuffer })
// Workerからの完了通知を受け取る
worker.addEventListener('message', (event) => {
console.log('Worker完了:', event.data)
console.log('共有バッファの最終値:', sharedArray[0])
// → Workerが書き込んだ値がリアルタイムで見える
})
>// worker.js - 共有バッファに書き込む
addEventListener('message', (event) => {
const { sharedBuffer } = event.data
const sharedArray = new Int32Array(sharedBuffer)
// Atomics.add で原子的にインクリメント(競合を防ぐ)
for (let i = 0; i < 10000; i++) {
Atomics.add(sharedArray, 0, 1) // インデックス0の値を+1(原子的操作)
}
console.log('Worker完了。10000回インクリメントしました')
postMessage({ done: true, finalValue: Atomics.load(sharedArray, 0) })
})
| メソッド | 説明 | 戻り値 |
|---|---|---|
Atomics.load(array, idx) | 指定インデックスの値を原子的に読み取る | 読み取り前の値 |
Atomics.store(array, idx, val) | 指定インデックスに値を原子的に書き込む | 書き込んだ値 |
Atomics.add(array, idx, val) | 指定インデックスに値を加算する | 加算前の値 |
Atomics.sub(array, idx, val) | 指定インデックスから値を減算する | 減算前の値 |
Atomics.compareExchange(array, idx, expected, replacement) | 期待値と一致する場合のみ置換する(CAS操作) | 置換前の値 |
Atomics.wait(array, idx, val, timeout) | 指定インデックスの値が変わるまで待機(WorkerのみOK) | 'ok'/'not-equal'/'timed-out' |
Atomics.notify(array, idx, count) | waitで待機しているWorkerに通知する | 通知したWorker数 |
>// parallel-main.js - 4つのWorkerで配列を並列処理
const SIZE = 1_000_000 // 100万要素
const sharedBuffer = new SharedArrayBuffer(SIZE * Float64Array.BYTES_PER_ELEMENT)
const sharedData = new Float64Array(sharedBuffer)
// データの初期化(乱数で埋める)
for (let i = 0; i < SIZE; i++) sharedData[i] = Math.random()
// 完了カウンター用の共有バッファ(別に用意)
const counterBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
const counter = new Int32Array(counterBuffer)
// 4つのWorkerを起動して配列を分割処理
const NUM_WORKERS = 4
const chunkSize = SIZE / NUM_WORKERS
const workers = Array.from({ length: NUM_WORKERS }, (_, i) => {
const worker = new Worker('./parallel-worker.js', { type: 'module' })
worker.postMessage({
sharedBuffer, // データバッファ(共有)
counterBuffer, // 完了カウンター(共有)
start: i * chunkSize,
end: (i + 1) * chunkSize,
workerId: i,
numWorkers: NUM_WORKERS,
})
return worker
})
// 全Worker完了をポーリングで検知
const checkInterval = setInterval(() => {
const doneCount = Atomics.load(counter, 0)
if (doneCount === NUM_WORKERS) {
clearInterval(checkInterval)
console.log('全Workerの処理完了')
console.log('先頭10件:', Array.from(sharedData.slice(0, 10)))
workers.forEach(w => w.terminate())
}
}, 10)
>// parallel-worker.js - 担当範囲のデータを処理する
addEventListener('message', (event) => {
const { sharedBuffer, counterBuffer, start, end, workerId } = event.data
const data = new Float64Array(sharedBuffer)
const counter = new Int32Array(counterBuffer)
// 担当範囲のデータに変換処理を適用(例:2乗)
for (let i = start; i < end; i++) {
data[i] = data[i] * data[i]
}
console.log(`Worker${workerId}: [${start}〜${end}] 処理完了`)
// 完了カウンターをインクリメント(原子的操作)
Atomics.add(counter, 0, 1)
})
複数のWorkerが同じメモリ領域に書き込む場合、「競合状態(Race Condition)」が発生する可能性があります。Atomics.compareExchangeを使ったMutex(相互排除ロック)を実装することで、一度に1つのWorkerのみが書き込めるように制御できます。
>// mutex.js - SharedArrayBufferを使ったMutexの実装
const UNLOCKED = 0
const LOCKED = 1
export class Mutex {
private lockBuffer: SharedArrayBuffer
private lockArray: Int32Array
constructor() {
this.lockBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
this.lockArray = new Int32Array(this.lockBuffer)
}
get sharedBuffer() {
return this.lockBuffer
}
// ロックを取得(取得できるまでスピン)
lock() {
while (true) {
// UNLOCKED → LOCKED に原子的に変換できたらロック取得成功
const prev = Atomics.compareExchange(this.lockArray, 0, UNLOCKED, LOCKED)
if (prev === UNLOCKED) {
return // ロック取得成功
}
// ロック取得失敗なら待機して再試行
Atomics.wait(this.lockArray, 0, LOCKED, 10) // 最大10ms待機
}
}
// ロックを解放
unlock() {
Atomics.store(this.lockArray, 0, UNLOCKED)
Atomics.notify(this.lockArray, 0, 1) // 待機中のWorker1つに通知
}
// ロックして処理を実行し、完了後にアンロック
withLock(fn: () => T): T {
this.lock()
try {
return fn()
} finally {
this.unlock()
}
}
}
>// shared-memory.ts - TypeScriptでの型付きSharedArrayBuffer活用
// 型付きSharedArrayBufferビューのユーティリティ
export function createSharedBuffer(
TypedArrayClass: new (buffer: SharedArrayBuffer) => T,
length: number
): { buffer: SharedArrayBuffer; view: T } {
const bytesPerElement = TypedArrayClass.BYTES_PER_ELEMENT
const buffer = new SharedArrayBuffer(length * bytesPerElement)
const view = new TypedArrayClass(buffer)
return { buffer, view }
}
type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array |
Int32Array | Uint32Array | Float32Array | Float64Array |
BigInt64Array | BigUint64Array
// 使用例
const { buffer: dataBuffer, view: dataView } =
createSharedBuffer(Float64Array, 1_000_000)
// Workerに送信
worker.postMessage({ dataBuffer })
// Atomicsを使った安全な読み書きラッパー
export function atomicUpdate(
array: Int32Array,
index: number,
updater: (current: number) => number
): number {
while (true) {
const current = Atomics.load(array, index)
const next = updater(current)
const prev = Atomics.compareExchange(array, index, current, next)
if (prev === current) return next // 成功
// 別Workerが先に更新した場合は再試行(スピンロック)
}
}
COEP(require-corpまたはcredentialless)を設定すると、クロスオリジンのリソース(外部CDN画像・Google Fonts・埋め込み動画など)の読み込み制限が厳しくなります。
>【COEP設定後に影響を受ける主なリソースと解決策】
❌ 影響が出る可能性があるもの:
→ 外部CDN(imgix・Cloudinary等)からの画像
→ crossorigin="anonymous" 属性を追加すれば解決
→ Google Fonts
→ require-corp では問題あり、credentialless では動作する
→ YouTube・Vimeo 埋め込み
→ credentialless で多くは解決
→ Stripe・Intercom等の外部SDKが使うiframe
→ sandbox="allow-same-origin" が必要になる場合がある
✅ 解決策:
1. COEP の値を "require-corp" → "credentialless" に変更
→ 多くのサードパーティリソースで問題が解消する
2. 外部リソースに crossorigin="anonymous" を追加


3. リソースのオーナーにCORPヘッダーを設定してもらう
Cross-Origin-Resource-Policy: cross-origin
4. 自社CDNにリソースをコピーしてクロスオリジン問題を回避
COEP の require-corp を設定すると、クロスオリジンのリソースは Cross-Origin-Resource-Policy: cross-origin ヘッダーを返すか、crossorigin="anonymous" 属性が必要になります。まず require-corp から credentialless に変更してください(Chromeは96+から対応)。それでも問題が残る場合は、該当リソースのCORSヘッダー対応状況を確認してください。
ArrayBufferをTransferableとして渡すと、所有権がWorkerに移動してメインスレッドからは使えなくなります。SharedArrayBufferは所有権移動なしで両者が同じメモリを参照します。頻繁に双方向で更新するデータにはSharedArrayBuffer、一方向に大量データを渡して結果を受け取る場合にはTransfer(Transferable)が適しています。
メインスレッドではAtomics.waitを使えません。メインスレッドはUIの描画をブロックしてはならないためです。メインスレッドから他のWorkerの処理完了を待つには、Atomics.waitAsync(Chrome 87+, Firefox 79+)を使うか、Worker側からpostMessageで完了を通知する方法が適切です。
はい、localhostはブラウザがセキュアコンテキストとして扱うため、COOP/COEPヘッダーなしでSharedArrayBufferを使えます。ただし本番デプロイ時にはヘッダーが必要になるため、開発段階からヘッダーを設定した状態でテストすることを推奨します。
WebAssemblyのスレッドモデル(pthread)はSharedArrayBufferをベースにしています。EmscriptenでCのpthreadコードをWasmにコンパイルする場合(-s USE_PTHREADS=1)、Wasm Workersが共有メモリを使うためSharedArrayBufferとCOOP/COEPが必須です。ffmpeg.wasmもこの仕組みを使っています。
same-origin、COEPに credentialless(またはrequire-corp)を設定することでcrossOriginIsolatedがtrueになる。credentiallessを使うとサードパーティリソースとの互換性が高い。add・compareExchange・wait・notifyが主要メソッド。Atomics.compareExchangeを使って複数Worker間の排他制御(Mutex)を実装できる。credentiallessを使うと多くのサードパーティリソースと互換性が保てる。Atomics.waitはメインスレッドでは使えない。SharedArrayBufferはWebAssemblyの並列処理や大規模データ処理に不可欠な技術です。COOP/COEPの設定はサーバー設定を数行追加するだけで完了します。credentiallessを使えばサードパーティリソースとの互換性も保ちながら有効化できます。まずローカル環境で動作確認してみましょう。
公式サイト より
今すぐ
無料カウンセリング
を予約!