WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

SharedArrayBufferとクロスオリジン分離(COOP/COEP)の実装方法【完全ガイド】|Web Workers並列処理・Atomics APIを徹底解説

この記事でわかること

  • SharedArrayBufferとは何か・通常のpostMessageとの違い
  • クロスオリジン分離(COOP/COEP)が必要な理由とSpectre脆弱性との関係
  • Next.js・Nginx・Vercel・Apache・CaddyでのCOOP/COEPヘッダー設定方法
  • Atomics APIを使った並列処理とMutexパターンの実装
  • サードパーティリソースとの互換性問題と解決策
生徒

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とは|通常のpostMessageと何が違うのか

SharedArrayBufferはES2017で追加されたJavaScript APIで、複数のWorkerが同一のメモリ領域を共有して読み書きできる仕組みです。通常のpostMessageがデータのコピーを作成して転送するのに対し、SharedArrayBufferはメモリの参照を共有するため、コピーコストがゼロです。

方式仕組みメモリ使用量双方向更新適したユースケース
postMessage(コピー)構造化クローンでデータをコピーして送信2倍(コピー分)可能(毎回コピー)小〜中規模データの一時的な受け渡し
Transferable(所有権移転)ArrayBufferの所有権を移転(コピーなし)変わらず不可(元スレッドで使えなくなる)大量データを一方向に渡す
SharedArrayBuffer(共有メモリ)複数のWorkerが同じメモリ空間を参照変わらず可能(コピーなし)Workerが同じバッファを並列読み書きする
>【SharedArrayBufferが特に有効なユースケース】

✅ 大量データのWorker間リアルタイム共有
✅ 複数Workerで分担して1つのバッファを並列処理
✅ WebAssemblyの並列処理(Emscriptenの-s USE_PTHREADS=1)
✅ 頻繁な双方向のデータ更新(ゲームの状態同期など)

クロスオリジン分離とは|なぜCOOP/COEPが必要か

2018年1月、Spectre脆弱性(CVE-2018-3640等)が公開されました。SharedArrayBufferを使うとJavaScriptで高精度なタイマーを作成でき、CPUのサイドチャネル攻撃によりクロスオリジンのデータ(パスワード・Cookieなど)を読み取れる可能性があります。

この脆弱性への対策として、ChromeはSharedArrayBufferを完全に無効化しました(2018〜2021年)。2021年以降、「クロスオリジン分離」という新しいセキュリティモデルが導入され、これを有効にした環境でのみSharedArrayBufferが再び使えるようになりました。

クロスオリジン分離(COOP/COEP)の仕組みを示す図
>【クロスオリジン分離を有効にするための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 になる

各サーバー・プラットフォームでのCOOP/COEP設定方法

Next.js(next.config.ts)

>// 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

Nginx

># /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;
    }
}

Vercel(vercel.json)

>{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
        { "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
      ]
    }
  ]
}

Apache(.htaccess)

># .htaccess


    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Cross-Origin-Embedder-Policy "credentialless"


# MIME タイプの設定(.wasm ファイルを使う場合)

    AddType application/wasm .wasm

Caddy(Caddyfile)

># Caddyfile

example.com {
    header Cross-Origin-Opener-Policy "same-origin"
    header Cross-Origin-Embedder-Policy "credentialless"

    file_server
    try_files {path} /index.html
}

SharedArrayBufferの実装コード|Atomicsによる並列処理

基本的な使い方

>// 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 APIの主要メソッド一覧

メソッド説明戻り値
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数

並列処理の実践例:大きな配列を複数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)
})

Mutexパターン|排他制御の実装

複数の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()
    }
  }
}

TypeScriptでのSharedArrayBuffer活用

>// 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にリソースをコピーしてクロスオリジン問題を回避

よくある質問(FAQ)

Q1. COEP を設定したら外部の画像が表示されなくなりました

COEP の require-corp を設定すると、クロスオリジンのリソースは Cross-Origin-Resource-Policy: cross-origin ヘッダーを返すか、crossorigin="anonymous" 属性が必要になります。まず require-corp から credentialless に変更してください(Chromeは96+から対応)。それでも問題が残る場合は、該当リソースのCORSヘッダー対応状況を確認してください。

Q2. SharedArrayBufferとTransferable Objectsの使い分けは?

ArrayBufferをTransferableとして渡すと、所有権がWorkerに移動してメインスレッドからは使えなくなります。SharedArrayBufferは所有権移動なしで両者が同じメモリを参照します。頻繁に双方向で更新するデータにはSharedArrayBuffer、一方向に大量データを渡して結果を受け取る場合にはTransfer(Transferable)が適しています。

Q3. Atomics.waitはメインスレッドでも使えますか?

メインスレッドではAtomics.waitを使えません。メインスレッドはUIの描画をブロックしてはならないためです。メインスレッドから他のWorkerの処理完了を待つには、Atomics.waitAsync(Chrome 87+, Firefox 79+)を使うか、Worker側からpostMessageで完了を通知する方法が適切です。

Q4. ローカル開発環境(localhost)でSharedArrayBufferは使えますか?

はい、localhostはブラウザがセキュアコンテキストとして扱うため、COOP/COEPヘッダーなしでSharedArrayBufferを使えます。ただし本番デプロイ時にはヘッダーが必要になるため、開発段階からヘッダーを設定した状態でテストすることを推奨します。

Q5. WebAssemblyの並列処理にSharedArrayBufferが必要な理由は?

WebAssemblyのスレッドモデル(pthread)はSharedArrayBufferをベースにしています。EmscriptenでCのpthreadコードをWasmにコンパイルする場合(-s USE_PTHREADS=1)、Wasm Workersが共有メモリを使うためSharedArrayBufferとCOOP/COEPが必須です。ffmpeg.wasmもこの仕組みを使っています。


まとめ

  • SharedArrayBuffer:複数のWorker間でメモリを共有する仕組み。コピーコストなしで並列処理が実現できる。WebAssemblyのスレッド処理にも必須。
  • クロスオリジン分離の必須条件:COOPに same-origin、COEPに credentialless(またはrequire-corp)を設定することでcrossOriginIsolatedがtrueになる。
  • サーバー設定:Next.js・Nginx・Vercel・Apache・Caddyすべてで数行の追記で有効化できる。credentiallessを使うとサードパーティリソースとの互換性が高い。
  • Atomics API:SharedArrayBufferへの読み書き競合を防ぐための原子的操作を提供する。addcompareExchangewaitnotifyが主要メソッド。
  • MutexパターンAtomics.compareExchangeを使って複数Worker間の排他制御(Mutex)を実装できる。
  • 注意点:COEP設定後は外部リソースにCORSヘッダーが必要。credentiallessを使うと多くのサードパーティリソースと互換性が保てる。Atomics.waitはメインスレッドでは使えない。

SharedArrayBufferはWebAssemblyの並列処理や大規模データ処理に不可欠な技術です。COOP/COEPの設定はサーバー設定を数行追加するだけで完了します。credentiallessを使えばサードパーティリソースとの互換性も保ちながら有効化できます。まずローカル環境で動作確認してみましょう。


WithCodeを体験できる初級コース公開中!

この記事を書いた人

WithCode(ウィズコード)は「目指すなら稼げる人材」をビジョンに、累計400名以上のフリーランスを輩出してきた超実践型プログラミングスクールです。150社以上の実案件支援を特徴にWeb制作・Webデザインなどの役立つ情報を現場のノウハウに基づいて発信していきます。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら
  • WithCareer
    - ウィズキャリ -

    就転職サポート

    大手エージェントのサポート下で
    キャリアアップを目指そう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次