Web Workerについて

JavaScriptは単一のスレッドで動作しているため、コードが実行されている間そのスレッドはブロックされます
そのため重い処理が行われるとブラウザがフリーズすることがあります

そこで

目次
  1. Web Workerについて
  2. 専用ワーカー
  3. 共有ワーカー
  4. Blobの使用
  5. Transferableインターフェイスについて
  6. OffscreenCanvas

Web Workerについて

Web WorkerはJavaScriptが実行されるWebブラウザ環境において、メインスレッドとは別のスレッドを作成して並列処理を行うためのAPIです

Web WorkerはWebアプリケーションが大量のデータを処理する必要がある場合やリアルタイムで更新される必要がある場合に有用です

  • Web Workerには専用ワーカー(Dedicated Worker)と共有ワーカー(Shared Worker)があります
  • ワーカースレッドは、メインスレッドと同じようにワーカー(サブワーカー)を作成することができます
  • ただし、Web Workerはスレッド間通信によってデータをやり取りする必要があり、通信コストはかかります
  • また、ワーカー内から直接DOMを操作ができないなどいくつかの制限があります

同じオリジン内のウェブページからのみWeb Workerにアクセスできます
注意:CSP(Content-Security-Policy)の制限について

Web Workerは他のドメインからのスクリプトの読み込みは禁止されていまが、Web Workerが読み込むスクリプトには、CSP(Content-Security-Policy)の制限が適用されます
*CSPはWebページに許可されたリソースのリスト
Web Workerが使用するスクリプトにCSPが適用される場合、Web WorkerはCSPが指定する許可されたリソースのみを読み込むことができます
Workerは自分を生成した文書(または親ワーカー)のCSPとは別に、自身のCSPを持つこともできます
*CSPに適合するようにするにはWebページのHTTPヘッダーまたはmetaタグにCSPを指定

専用ワーカー

専用ワーカーは呼び出し元のスクリプトだけがアクセスできます

worker.jsファイルをロードして、専用ワーカーを作成
*URIは同一オリジンであること

//ワーカーの生成(ワーカースレッドで実行するスクリプトのURI)
const worker = new Worker('worker.js');
  • ワーカーとメインスレッドの間でデータをやり取りするには、messsageイベントが使用されます
    *onmessage イベントハンドラーを使用することもできます
    postMessage() メソッドを使用してメッセージを送信します
    データは messsageイベントのdata属性から取得します
  • errorイベントもあり、ワーカースレッドで例外が投げられると発生します
    *onerrorイベントハンドラーを使用することもできます
  • ワーカー内でスクリプトをインポートするには、importScripts()関数を使用します
    importScripts()関数は引数として渡されたJavaScriptファイルを同期的に読み込み、グローバルスコープ内で実行します
    複数の引数を受け取ることもでき、順番に読み込みます
    *同じドメイン内のファイルであれば、相対パスで指定できます
 // ワーカーの作成
const worker = new Worker( "worker.js" );
//ワーカーにメッセージを送信
worker.postMessage(10);
console.log('ワーカーにデータを送信')

// ワーカーから受信
worker.addEventListener( "message" , e =>{
console.log(`ワーカからデータ(${e.data})を受信`)
});

// ワーカーからのエラー通知を受信
worker.addEventListener( "error" , e =>{
console.log( e.message );
});

// ワーカーを終了する
// worker.terminate();
/*workerではworker自身がグローバルです
selfは省略できます*/
//スクリプトファイルの読み込み
self.importScripts("isnumber.js");
//メインスレッドから受信
self.addEventListener( "message" , m =>{
if( !isNumber( m.data ) ) throw new Error("数値ではない");
  self.postMessage( m.data );
});
function isNumber(value) {
  return typeof value === 'number';
}

共有ワーカー

共有ワーカーはオリジンが同一であれば(異なるタブ、iframe、ワーカーなどからでも)複数のスクリプトからアクセスできます

共有ワーカーの作成

const worker = new SharedWorker('worker.js');

共有ワーカーではportオブジェクトを通して通信します
portプロパティにイベント登録します

// 共有ワーカーの作成または存在確認
const worker = new SharedWorker("worker.js");

// 共有ワーカーからの通知を受信
worker.port.addEventListener( "message" , e =>{
    console.log( e.data );
});

// 共有ワーカーからエラーを受信
worker.addEventListener( "error" , e =>{
    console.log( e.message );
});

//通信開始
worker.port.start();

//共有ワーカーにメッセージを送信
worker.port.postMessage(10);

//共有ワーカーとの接続解除
// worker.port.close();
self.addEventListener( "connect" , e =>{
    const port = e.ports[0];  //ポートを記憶、メインスレッドとの通信はこのポートで行う
    port.addEventListener( "message" , e =>{
        if( !isNumber( e.data ) ) throw new Error("数値ではない");
        port.postMessage( e.data );
    });
    port.start();
});

self.importScripts("isnumber.js");

複数のスレッドで処理で注意すること(並列処理とデータ同期)

  • 並列処理とは、同時に複数の処理を実行することで、複数のスレッドやプロセスを使用して、それぞれが異なるタスクを実行することで高速化を図ります
  • データ同期は、複数の処理が同じデータを共有し、そのデータが一貫性を保って更新されることを保証することです

複数のスレッドやプロセスが同じデータにアクセスすると、競合状態が発生する可能性があり、データの不整合や処理の不正確さ、プログラムのクラッシュなどが発生する可能性があります
MessageChannel APIは複数のメッセージを順番に処理する必要がある場合に便利です
メッセージを受信した後に応答を返すことができ、非同期通信を効率的に行うことができます

Blobの使用

Blobはバイナリデータ(0と1)を表すためのJavaScriptオブジェクトです

  • Blobは大きなデータを扱うときに役立ちます
    データをメモリ内に一時的に保持する必要がなくメモリ使用量を削減できます
  • Blobを使用するとファイルを直接読み込んだり、ファイルを生成したり、ファイルをダウンロードしたりできます
  • さまざまな種類のファイルやデータを処理することができます
    Blobはテキスト、画像、音声、ビデオなど、さまざまなファイル形式に対応しています。

JavaScriptでBlobを扱うときの基本

/*Blobの作成
第1引数はBlobオブジェクトに含めるデータ、第2引数はBlobのMIMEタイプ
*/
const data = 'Hello, world!';
const blob = new Blob([data], { type: 'text/plain' });

/*Blobの読み取り
Blobオブジェクトに含まれるデータを読み取るには、FileReaderオブジェクトを使用します
readAsText()メソッドでテキストとして読み取り、その結果をonloadハンドラーで処理
*/
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const reader = new FileReader();
reader.onload = (event) => {
  console.log(event.target.result);
};
reader.readAsText(blob);

/*Blobのダウンロード
URLを使ってダウンロード用のリンクを作成しています。
download属性を使用して、ダウンロード時のファイル名を指定
click()メソッドを使用して、リンクをクリックしてダウンロードを開始します
*/
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'file.txt';
link.click();

余談:ダウンロードリンクを作成

<!-- ダウンロードリンクを表示するボタン -->
<button id="download-button">Download</button>

<script>
  // ボタンをクリックしたときの処理
  document.getElementById('download-button').addEventListener('click', () => {
  // ダウンロードするファイルの内容を作成
  const content = 'This is the content of the file.';
  // Blobオブジェクトを作成
  const blob = new Blob([content], { type: 'text/plain' });
  // URLを生成
  const url = URL.createObjectURL(blob);
  // ダウンロードリンクを作成
  const link = document.createElement('a');
  link.href = url;
  link.download = 'file.txt';
  document.body.appendChild(link);
  // リンクをクリックしてダウンロードを開始
  link.click();
  // 不要になったURLを解放
  URL.revokeObjectURL(url);
  // リンクを削除
  link.remove();
});
</script>

Web WorkerでBlobを使用する例
*ファイルを選択するとメインスレッドでBlobオブジェクトを作成してWeb Workerに送信
Web Workerは、DataURL形式の文字列に変換しメインスレッドに送信

<input type="file" id="file-input">
<div id="result"></div>
<script>
 const fileInput = document.getElementById('file-input');
 const resultElement = document.getElementById('result');

fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  const reader = new FileReader();
  reader.readAsArrayBuffer(file);
  reader.onload = () => {
    const arrayBuffer = reader.result;
    const blob = new Blob([arrayBuffer], { type: file.type });
    const worker = new Worker('worker.js');
    worker.postMessage(blob);
    worker.onmessage = (event) => {
      const base64 = event.data;
      const img = document.createElement('img');
      img.src = base64;
      resultElement.appendChild(img);
   };
  };
});
</script>
self.onmessage = (event) => {
    const data = event.data;
    if (data instanceof Blob) {
      const reader = new FileReader();
//readAsDataURLはBlobオブジェクトをデータURLとして読み取り、文字列として返します
      reader.readAsDataURL(data);
      reader.onload = () => {
        const base64 = reader.result;
        self.postMessage(base64);
      };
    } else {
      console.error('Invalid data format');
    }
  };

ArrayBufferオブジェクトについて
ArrayBufferオブジェクトは固定長のバイナリデータを表すJavaScriptのビルトインオブジェクトです
*バイナリデータはメモリ上に格納されデータに直接アクセスできます

ArrayBufferオブジェクトの内容にアクセスするための手段

  • TypedArrayオブジェクト
    • Int8Array:8ビット整数を表します(範囲は-128から127)
    • Uint8Array:符号なし8ビット整数を表します(範囲は0から255)
    • Int16Array:16ビット整数を表します (範囲は-32768から32767)
    • Uint16Array:符号なし16ビット整数を表します(範囲は0から65535)
    • Int32Array:32ビット整数を表します(範囲は-2147483648から2147483647)
    • Uint32Array:符号なし32ビット整数を表します(範囲は0から4294967295)
    • Float32Array:単精度浮動小数点数を表します
    • Float64Array:倍精度浮動小数点数を表します
  • DataViewオブジェクト
    ビット幅を指定する必要はありません

メインスレッドで配列をUint8Arrayに変換してWeb Workerとの間でやり取りする例

const worker = new Worker("worker.js");
// 配列を生成する
const intArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// 配列をUint8Arrayに変換する
const uint8Array = new Uint8Array(intArray);
// Uint8ArrayをWeb Workerに送信する
worker.postMessage(uint8Array);

// Web Workerからの結果を受信する
worker.onmessage = function(e) {
  console.log(e.data);
};
self.onmessage = function(e) {
    // 受信したUint8Arrayを配列に変換する
    const intArray = Array.from(e.data);
    // 配列の各要素に2を乗じる
    const resultArray = intArray.map(value=> value * 2);
    // 変換後の配列をUint8Arrayに変換する
    const resultUint8Array = new Uint8Array(resultArray);
    // Uint8Arrayをメインスレッドに送信する
    postMessage(resultUint8Array);
  };

JavaScriptファイルをBlobに変換して、Workerコンストラクタに渡すことができます
Blobを使用して外部ファイルを読み込まずにWeb Workerのコードを動的に生成する
URL.createObjectURL()を使用してBlob URLを作成します

const worker = new Worker(URL.createObjectURL(
 new Blob([`
  self.addEventListener('message', function(e) {
    console.log( e.data);
    self.postMessage('ワーカーからのメッセージ');
  }, false);
`])
));
worker.postMessage('ワーカーへのメッセージ');
worker.addEventListener('message', function(e) {
  console.log(e.data);
}, false);

Transferableインターフェイスについて

Transferableインターフェースを使用すると、Web Workerからメインスレッドにデータを送信するときにオブジェクトを転送できます(コピーするためのコストを削減)

Transferableインターフェースはデータ(ArrayBuffer・MessagePort・ImageBitmap・OffscreenCanvas)を転送するためにコピーを作成せず、オブジェクトの所有権を移動します
これによりデータを転送する際のパフォーマンスが向上しメモリの消費を抑えます
注意点は、転送元でオブジェクトを使用する場合は、clone()メソッドを使用してコピーを作成する必要があります

postMessageの引数について

worker.postMessage(message, [transfer]);
  • message:ワーカーに送るオブジェクト
    文字列、配列、数値、真偽値などほぼすべてのデータ型
  • transfer:オプションで、所有権を移転する移転可能オブジェクト(ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvasなど)の配列
    オブジェクトの所有権が移転されると、そのオブジェクトは送信元のコンテキストでは使用できなくなり、送信先のワーカーのみが使用できるようになります。
    *コピーするのではなく所有権がWeb Workerに移動します(メモリ使用量の削減)

大きなデータの転送の例

// 大きなデータを作成する
const bigArray = new Float32Array(1000000);
// Web Workerを作成する
const worker = new Worker('worker.js');
// Web Workerに大きなデータを転送する
worker.postMessage({ data: bigArray.buffer }, [bigArray.buffer]);
// Web Workerからのメッセージを受信する
worker.onmessage = (event) => {
  console.log('Web Workerからのメッセージ:', event.data);
};
// メインスレッドからのメッセージを受信する
onmessage = (event) => {
  // 大きなデータを受信する
  const data = new Float32Array(event.data.data);
  // 大きなデータを処理する
  const result = processData(data);
  // メインスレッドに結果を送信する
  postMessage({ result });
};
function processData(data) {
  // データを処理する
  // ...
  return data
}

OffscreenCanvas

オフスクリーンでレンダリングできる、DOMから切り離された「Canvas」です

OffscreenCanvasをメインスレッド利用するケース
*OffscreenCanvasを使用して事前にレンダリングされたグラフィックスをキャッシュする場合やOffscreenCanvasを画像ファイルとして保存する場合など
例ではOffscreenCanvasを使用して画像をグレースケールに変換します
*OffscreenCanvasオブジェクトのコンテキストに対して行った描画命令は自動的にもとのCanvas要素に反映されます

<img src="/image.jpg" id="image" style="display: none;">
<canvas id="canvas"></canvas>
<script>
  const image = document.getElementById('image');
  const canvas = document.getElementById('canvas');

  // image要素の読み込み完了時にcanvas要素のサイズを設定する
  image.addEventListener('load', () => {
    canvas.width = image.width;
    canvas.height = image.height;

    const offscreenCanvas = canvas.transferControlToOffscreen();
    // OffscreenCanvasのコンテキストを取得する
    const offscreenContext = offscreenCanvas.getContext('2d');
   // 画像を描画する
    offscreenContext.drawImage(image, 0, 0);
   // ピクセルデータを取得する
    const imageData = offscreenContext.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;
    // グレースケールに変換する
    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      const average = (r + g + b) / 3;
      data[i] = average;
      data[i + 1] = average;
      data[i + 2] = average;
    }
   // 変換したピクセルデータをOffscreenCanvasに描画する
    offscreenContext.putImageData(imageData, 0, 0);
  });
</script>

workerスレッドでOffscreenCanvasを使用する
まず、メインスレッドからworkerスレッドにOffscreenCanvasを渡す
workerスレッドでOffscreenCanvasを生成して描画する
*workerスレッドで描画できます

// Canvas要素を取得する
const canvas = document.getElementById("canvas");
// Canvas要素の描画コントロールをOffscreenCanvasに委譲する
const offscreenCanvas = canvas.transferControlToOffscreen();
// Workerを作成し、OffscreenCanvasを渡す
const worker = new Worker("worker.js");
worker.postMessage({ canvas: offscreenCanvas }, [
  offscreenCanvas
]);
onmessage = (event) => {
    if (event.data.canvas){
        const offscreen = event.data.canvas;
        const context = offscreen.getContext('2d');
        context.fillStyle = 'blue';
        context.fillRect(0, 0, 100, 100);
    }
  };

OffscreenCanvasのブラウザ対応状況(2023年3月現在)

Can I use(OffscreenCanvas)