JavaScript/Worker

Worker オブジェクト

1. Worker とは

Worker(ワーカー)は、メインのJavaScriptプログラム実行スレッドとは別に、バックグラウンドでJavaScriptを実行するための仕組みです。Workerを使用することで、CPU負荷の高い処理をメインスレッドから分離し、ブラウザの応答性を維持することができます。

1.1 Worker の種類

Workerには主に以下の種類があります:

Worker の種類 特徴 用途
Dedicated Worker 特定のスクリプトにのみ紐づく 単一のウェブページ内の処理
Shared Worker 複数のスクリプト間で共有可能 複数のウィンドウやIFrame間での共有処理
Service Worker ページやネットワークリクエストの間に存在 オフラインキャッシュ、プッシュ通知など

本章では主にDedicated Workerについて解説します。

2. Worker の基本的な使い方

2.1 Worker の作成

Worker を作成するには、Worker コンストラクタを使用します。コンストラクタの引数には、Worker で実行するJavaScriptファイルのパスを指定します。

// メインスクリプト(main.js)
const myWorker = new Worker('worker.js');

2.2 Worker との通信

Worker とメインスクリプト間の通信は、メッセージパッシング方式で行います。 メインスクリプトから Worker にメッセージを送るには postMessage() メソッドを使用します:

// メインスクリプト(main.js)
const myWorker = new Worker('worker.js');

// Workerにメッセージを送信
myWorker.postMessage({ command: 'start', data: [1, 2, 3, 4, 5] });

Worker からのメッセージを受け取るには、message イベントをリスニングします:

// メインスクリプト(main.js)
myWorker.addEventListener('message', function(event) {
  console.log('Workerからの結果:', event.data);
});

// または省略形で
myWorker.onmessage = function(event) {
  console.log('Workerからの結果:', event.data);
};

2.3 Worker側のコード

Worker側では、self オブジェクトを通じてメッセージを受け取り、処理結果を返します:

// worker.js
self.addEventListener('message', function(event) {
  // メインスクリプトからのメッセージを受け取る
  const data = event.data;
  
  if (data.command === 'start') {
    // 何らかの処理を実行
    const result = processData(data.data);
    
    // 処理結果をメインスクリプトに返す
    self.postMessage({ status: 'completed', result: result });
  }
});

function processData(array) {
  // 重い処理の例:配列の各要素を2乗
  return array.map(num => num * num);
}

2.4 Worker の終了

Worker の処理が不要になった場合は、terminate() メソッドを呼び出して終了させることができます:

// メインスクリプト(main.js)
myWorker.terminate();

Worker側から自身を終了する場合は、self.close() を使用します:

// worker.js
self.close();

3. Worker の制約と注意点

3.1 DOM操作の禁止

Worker はDOM(Document Object Model)にアクセスできません。つまり、Worker内から直接HTML要素を操作することはできません。

// worker.js - これはエラーになる
const element = document.getElementById('myElement'); // エラー: document is not defined

3.2 利用可能なAPIと利用できないAPI

Worker内では、以下のようなAPIが利用可能です:

利用可能なAPI
通信関連 fetch(), XMLHttpRequest
タイマー setTimeout(), setInterval()
データストレージ IndexedDB
暗号化 SubtleCrypto
文字列・配列操作 String, Array のメソッド

一方、以下のAPIは利用できません:

利用できないAPI 理由
window オブジェクト Worker はウィンドウコンテキストを持たない
document オブジェクト DOM 操作は禁止
parent オブジェクト 親子関係の概念がない
alert(), confirm() など UI操作はメインスレッドの責務

3.3 Worker間のデータ転送

データをWorkerに転送する際は、データのコピーが発生します。大きなデータを扱う場合はパフォーマンスに影響する可能性があります。この問題を解決するために Transferable Objects が提供されています:

// 大きなバッファを転送する例
const buffer = new ArrayBuffer(1024 * 1024 * 32); // 32MB
myWorker.postMessage({ data: buffer }, [buffer]);

2番目の引数にTransferableなオブジェクトの配列を指定すると、コピーではなく所有権の移転(転送)となります。転送後、元のスコープでは使用できなくなることに注意してください。

4. 実践的な使用例

4.1 画像処理の例

画像処理のような重い処理をWorkerに任せる例です:

// メインスクリプト
const imageWorker = new Worker('image-processor.js');

// 画像データを取得
const canvas = document.getElementById('sourceCanvas');
const imageData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);

// Workerに処理を依頼
imageWorker.postMessage({
  imageData: imageData,
  filter: 'grayscale'
});

// 処理結果を受け取って表示
imageWorker.onmessage = function(event) {
  const resultCanvas = document.getElementById('resultCanvas');
  const ctx = resultCanvas.getContext('2d');
  ctx.putImageData(event.data.imageData, 0, 0);
};
// image-processor.js
self.onmessage = function(event) {
  const imageData = event.data.imageData;
  const filter = event.data.filter;
  
  // フィルター適用の処理
  if (filter === 'grayscale') {
    applyGrayscale(imageData);
  }
  
  // 処理結果を返す
  self.postMessage({ imageData: imageData });
};

function applyGrayscale(imageData) {
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i]     = avg; // 赤
    data[i + 1] = avg; // 緑
    data[i + 2] = avg; // 青
    // data[i + 3] はアルファ値なので変更しない
  }
}

4.2 大量データの計算例

大量のデータを処理する例として、素数を計算するWorkerを作成します:

// メインスクリプト
const primeWorker = new Worker('prime-calculator.js');

document.getElementById('calculateBtn').addEventListener('click', function() {
  const max = parseInt(document.getElementById('maxNumber').value);
  
  // 計算開始時刻を記録
  const startTime = performance.now();
  
  // UI更新
  document.getElementById('status').textContent = '計算中...';
  
  // Workerに計算を依頼
  primeWorker.postMessage({ max: max });
});

primeWorker.onmessage = function(event) {
  const endTime = performance.now();
  const primes = event.data.primes;
  const count = primes.length;
  
  // 結果表示
  document.getElementById('status').textContent = 
    `計算完了:${count}個の素数を発見(処理時間:${(endTime - event.data.startTime).toFixed(2)}ms)`;
  document.getElementById('result').textContent = 
    primes.slice(0, 100).join(', ') + (count > 100 ? '...' : '');
};
// prime-calculator.js
self.onmessage = function(event) {
  const max = event.data.max;
  const startTime = performance.now();
  
  // 素数を計算
  const primes = calculatePrimes(max);
  
  // 結果を返す
  self.postMessage({
    primes: primes,
    startTime: startTime
  });
};

function calculatePrimes(max) {
  const sieve = [];
  const primes = [];
  
  // エラトステネスのふるい
  for (let i = 2; i <= max; i++) {
    if (!sieve[i]) {
      primes.push(i);
      for (let j = i << 1; j <= max; j += i) {
        sieve[j] = true;
      }
    }
  }
  
  return primes;
}

5. Shared Worker の使い方

Shared Workerは複数のブラウジングコンテキスト(タブ、ウィンドウ、iframeなど)間で共有できるWorkerです。

5.1 Shared Worker の作成と通信

// メインスクリプト(複数のタブやウィンドウで動作する)
const sharedWorker = new SharedWorker('shared-worker.js');

// ポート経由で通信
sharedWorker.port.start();
sharedWorker.port.postMessage({ action: 'register', id: 'window1' });

// メッセージ受信
sharedWorker.port.onmessage = function(event) {
  console.log('Shared Workerからのメッセージ:', event.data);
};
// shared-worker.js
const connections = [];

// 接続イベント
self.onconnect = function(event) {
  const port = event.ports[0];
  connections.push(port);
  
  port.start();
  
  // このポートからのメッセージを処理
  port.onmessage = function(event) {
    const data = event.data;
    
    if (data.action === 'register') {
      // すべての接続に通知
      connections.forEach(function(connection) {
        connection.postMessage({
          type: 'notification',
          message: `新しい接続が確立されました: ${data.id}`,
          connectionsCount: connections.length
        });
      });
    }
  };
};

6. Service Worker の基本

Service Workerはブラウザとネットワーク間に位置するプロキシサーバーのような役割を果たします。オフラインサポート、プッシュ通知、バックグラウンド同期などの機能を実現できます。

6.1 Service Worker の登録

// メインスクリプト
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(function(registration) {
      console.log('Service Worker 登録成功:', registration.scope);
    })
    .catch(function(error) {
      console.log('Service Worker 登録失敗:', error);
    });
}
// service-worker.js
self.addEventListener('install', function(event) {
  console.log('Service Worker インストール中...');
  
  // キャッシュの準備
  event.waitUntil(
    caches.open('my-cache-v1').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
        '/images/logo.png'
      ]);
    })
  );
});

self.addEventListener('activate', function(event) {
  console.log('Service Worker アクティブ化');
  
  // 古いキャッシュの削除
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          return cacheName !== 'my-cache-v1';
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

self.addEventListener('fetch', function(event) {
  console.log('Fetch イベント:', event.request.url);
  
  // キャッシュファーストの戦略
  event.respondWith(
    caches.match(event.request).then(function(response) {
      // キャッシュにあればそれを返す
      if (response) {
        return response;
      }
      
      // なければネットワークリクエスト
      return fetch(event.request).then(function(response) {
        // レスポンスのコピーをキャッシュに保存
        if (response.status === 200) {
          const responseToCache = response.clone();
          caches.open('my-cache-v1').then(function(cache) {
            cache.put(event.request, responseToCache);
          });
        }
        
        return response;
      });
    })
  );
});

7. Worker の高度なトピック

7.1 Web Worker モジュール (ES Modules)

最新のブラウザでは、ES Modulesを使用したWorkerの作成がサポートされています:

// メインスクリプト
const moduleWorker = new Worker('module-worker.js', {
  type: 'module'
});
// module-worker.js
import { calculateStatistics } from './statistics.js';

self.onmessage = async function(event) {
  const data = event.data;
  const result = await calculateStatistics(data);
  self.postMessage(result);
};

7.2 Worklet

Workletは、レンダリングパイプラインの特定の部分にフックするための軽量のSpecialized Workerです。Audio WorkletやPaint Workletなどがあります。

// Paint Worklet の例
CSS.paintWorklet.addModule('my-paint-worklet.js');
// my-paint-worklet.js
class MyPainter {
  paint(ctx, size, properties) {
    // カスタムペイント処理
    ctx.fillStyle = 'green';
    ctx.fillRect(0, 0, size.width, size.height);
  }
}

registerPaint('myPainter', MyPainter);

7.3 デバッグ

Worker内でのデバッグは、開発者ツールの「Sources」タブで行えます。ブレークポイントを設定したり、コンソールログを確認したりできます。

// worker.js
console.log('Worker が起動しました');
debugger; // デバッガー停止ポイント

8. Worker のパフォーマンスとベストプラクティス

8.1 Worker の使いどころ

適している処理 適していない処理
画像・動画処理 DOM操作が必要な処理
大量データの計算・解析 UIと密接に関連する処理
暗号化・複合化 短時間で終わる軽量な処理
テキスト処理(構文解析など) 頻繁に小さなデータをやり取りする処理

8.2 メモリとパフォーマンスの最適化

  1. Transferable Objects の活用:大きなデータを扱う場合は、コピーではなく転送を使用しましょう。
  2. 適切な粒度の処理:あまりに小さな処理をWorkerに委託すると、通信オーバーヘッドが処理時間を上回ることがあります。
  3. Worker プール:多数の小さなタスクを処理する場合は、複数のWorkerを管理するプールを作成するとよいでしょう。
// Worker プールの簡易実装
class WorkerPool {
  constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.availableWorkers = [];
    this.queue = [];
    
    for (let i = 0; i < size; i++) {
      const worker = new Worker(workerScript);
      worker.onmessage = this.onWorkerMessage.bind(this, worker);
      this.workers.push(worker);
      this.availableWorkers.push(worker);
    }
  }
  
  onWorkerMessage(worker, event) {
    // 結果をコールバックに渡す
    const callback = worker.currentCallback;
    delete worker.currentCallback;
    
    // Workerを利用可能プールに戻す
    this.availableWorkers.push(worker);
    
    // キューにタスクがあれば実行
    if (this.queue.length > 0) {
      const task = this.queue.shift();
      this.runTask(task.data, task.callback);
    }
    
    // コールバック実行
    if (callback) {
      callback(event.data);
    }
  }
  
  runTask(data, callback) {
    if (this.availableWorkers.length === 0) {
      // 利用可能なWorkerがない場合はキューに追加
      this.queue.push({ data, callback });
      return;
    }
    
    // 利用可能なWorkerを取得
    const worker = this.availableWorkers.pop();
    worker.currentCallback = callback;
    worker.postMessage(data);
  }
  
  terminate() {
    this.workers.forEach(worker => worker.terminate());
    this.workers = [];
    this.availableWorkers = [];
    this.queue = [];
  }
}

// 使用例
const pool = new WorkerPool('worker.js', 4);
pool.runTask({ data: [1, 2, 3] }, result => {
  console.log('結果:', result);
});

9. まとめ

Workerオブジェクトは、JavaScriptの単一スレッドという制約を克服し、マルチスレッドプログラミングを可能にする強力な機能です。適切に使用することで、ウェブアプリケーションのパフォーマンスと応答性を大幅に向上させることができます。

Workerの種類と使い分け、通信方法、制約とベストプラクティスを理解することで、より効率的でユーザーフレンドリーなウェブアプリケーションを開発することができるでしょう。

重要なポイントを振り返りましょう:

  1. Workerは重い処理をバックグラウンドで実行し、メインスレッドの応答性を維持します。
  2. Workerとの通信はメッセージパッシングで行われます。
  3. WorkerはDOM操作ができないという大きな制約があります。
  4. 大きなデータを扱う場合は、Transferable Objectsを使用してください。
  5. 用途に応じてDedicated Worker、Shared Worker、Service Workerを使い分けましょう。
  6. Worker プールを実装することで、複数のタスクを効率的に処理できます。

Workerオブジェクトを活用して、より高性能なウェブアプリケーションを開発していきましょう。

附録

静的プロパティ

Worker.arguments
Worker.caller
Worker.length
Worker.name
Worker.prototype

静的アクセサ

静的メソッド

Workerのインスタンスプロパティ

Worker.prototype [ Symbol.toStringTag ]

Workerのインスタンスアクセサ

get Worker.prototype.onerror
get Worker.prototype.onmessage

Workerのインスタンスメソッド

Worker.prototype.constructor()
Worker.prototype.postMessage()
Worker.prototype.terminate()

EventTargetのインスタンスプロパティ

EventTarget.prototype [ Symbol.toStringTag ]

EventTargetのインスタンスアクセサ

EventTargetのインスタンスメソッド

EventTarget.prototype.addEventListener()
EventTarget.prototype.constructor()
EventTarget.prototype.dispatchEvent()
EventTarget.prototype.removeEventListener()
カテゴリ:JavaScript
カテゴリ:JavaScript