具体的なウェブワーカーのユースケース

前回のモジュールでは、ウェブ ワーカーの概要について説明しました。ウェブ ワーカーは、JavaScript をメインスレッドから別のウェブ ワーカー スレッドに移動することで、入力の応答性を高めることができます。これにより、メインスレッドに直接アクセスする必要のない作業がある場合に、ウェブサイトのインタラクションから次の描画までの時間(INP)を改善できます。ただし、概要だけでは十分ではありません。このモジュールでは、ウェブ ワーカーの具体的なユースケースを紹介します。

たとえば、画像から Exif メタデータを削除する必要があるウェブサイトが考えられます。これは、それほど突飛なコンセプトではありません。実際、Flickr などのウェブサイトでは、ユーザーが Exif メタデータを表示して、ホストされている画像の技術的な詳細(色深度、カメラのメーカーとモデルなど)を確認できるようになっています。

ただし、画像をフェッチして ArrayBuffer に変換し、Exif メタデータを抽出するロジックをすべてメインスレッドで実行すると、コストが高くなる可能性があります。幸いなことに、ウェブ ワーカーのスコープでは、この処理をメインスレッドから外して実行できます。次に、ウェブ ワーカーのメッセージ パイプラインを使用して、Exif メタデータが HTML 文字列としてメインスレッドに送信され、ユーザーに表示されます。

ウェブ ワーカーを使用しない場合のメインスレッド

まず、ウェブ ワーカーを使用せずにこの処理を行った場合のメインスレッドの様子を確認します。これを行う方法は次のとおりです。

  1. Chrome で新しいタブを開き、その DevTools を開きます。
  2. パフォーマンス パネルを開きます。
  3. https://chrome.dev/learn-performance-exif-worker/without-worker.html に移動します。
  4. パフォーマンス パネルで、DevTools ペインの右上にある [Record] をクリックします。
  5. フィールドにこの画像リンク(または Exif メタデータを含む別の画像リンク)を貼り付け、[Get that JPEG!] ボタンをクリックします。
  6. インターフェースに Exif メタデータが入力されたら、[Record] をもう一度クリックして録画を停止します。
パフォーマンス プロファイラ。画像メタデータ抽出アプリのアクティビティがすべてメインスレッドで発生していることを示しています。2 つの大きな長時間タスクがあります。1 つはリクエストされた画像を取得してデコードするフェッチを実行するタスク、もう 1 つは画像からメタデータを抽出するタスクです。
画像メタデータ抽出アプリのメインスレッド アクティビティ。すべてのアクティビティがメインスレッドで発生していることに注意してください。

ラスタライザー スレッドなどの他のスレッドが存在する場合を除き、アプリ内のすべての処理はメインスレッドで実行されます。メインスレッドでは、次の処理が行われます。

  1. フォームは入力を受け取り、fetch リクエストをディスパッチして、Exif メタデータを含む画像の最初の部分を取得します。
  2. 画像データは ArrayBuffer に変換されます。
  3. exif-reader スクリプトは、画像から Exif メタデータを抽出するために使用されます。
  4. メタデータがスクレイピングされて HTML 文字列が構築され、メタデータ ビューアにデータが入力されます。

次に、同じ動作をウェブ ワーカーを使用して実装した場合を見てみましょう。

ウェブ ワーカーありのメインスレッド

メインスレッドで JPEG ファイルから Exif メタデータを抽出する際の処理を確認しました。次に、ウェブ ワーカーが関与する場合の処理を見てみましょう。

  1. Chrome で別のタブを開き、その DevTools を開きます。
  2. パフォーマンス パネルを開きます。
  3. https://chrome.dev/learn-performance-exif-worker/with-worker.html に移動します。
  4. パフォーマンス パネルで、DevTools ペインの右上にある録画ボタンをクリックします。
  5. フィールドにこの画像リンクを貼り付け、[Get that JPEG!] ボタンをクリックします。
  6. インターフェースに Exif メタデータが入力されたら、もう一度録画ボタンをクリックして録画を停止します。
メインスレッドとウェブ ワーカー スレッドの両方で発生している画像メタデータ抽出アプリのアクティビティを示すパフォーマンス プロファイラ。メインスレッドで実行されるタスクはまだありますが、大幅に短縮され、画像の取得/デコードとメタデータの抽出はすべてウェブ ワーカー スレッドで行われるようになりました。メインスレッドの処理は、ウェブ ワーカーとの間でデータを渡すことだけです。
画像メタデータ抽出アプリのメインスレッド アクティビティ。ほとんどの作業が行われる追加のウェブ ワーカー スレッドがあることに注意してください。

これがウェブ ワーカーの力です。メインスレッドですべてを行うのではなく、メタデータ ビューアへの HTML の入力以外のすべてが別のスレッドで行われます。つまり、メインスレッドは他の処理を実行できるようになります。

ここで最も大きなメリットは、ウェブ ワーカーを使用しないこのアプリのバージョンとは異なり、exif-reader スクリプトがメインスレッドではなくウェブ ワーカー スレッドに読み込まれることです。つまり、exif-reader スクリプトのダウンロード、解析、コンパイルのコストはメインスレッド外で発生します。

それでは、これらを実現するウェブ ワーカーのコードを見ていきましょう。

ウェブ ワーカーコードの概要

ウェブ ワーカーの差を確認するだけでは不十分です。少なくともこのケースでは、ウェブ ワーカーのスコープで何が可能かを知るために、コードがどのようなものか理解しておく必要があります。

ウェブ ワーカーが処理を開始する前に実行する必要があるメインスレッドのコードから始めます。

// scripts.js

// Register the Exif reader web worker:
const exifWorker = new Worker('/js/with-worker/exif-worker.js');

// We have to send image requests through this proxy due to CORS limitations:
const imageFetchPrefix = 'https://res.cloudinary.com/demo/image/fetch/';

// Necessary elements we need to select:
const imageFetchPanel = document.getElementById('image-fetch');
const imageExifDataPanel = document.getElementById('image-exif-data');
const exifDataPanel = document.getElementById('exif-data');
const imageInput = document.getElementById('image-url');

// What to do when the form is submitted.
document.getElementById('image-form').addEventListener('submit', event => {
  // Don't let the form submit by default:
  event.preventDefault();

  // Send the image URL to the web worker on submit:
  exifWorker.postMessage(`${imageFetchPrefix}${imageInput.value}`);
});

// This listens for the Exif metadata to come back from the web worker:
exifWorker.addEventListener('message', ({ data }) => {
  // This populates the Exif metadata viewer:
  exifDataPanel.innerHTML = data.message;
  imageFetchPanel.style.display = 'none';
  imageExifDataPanel.style.display = 'block';
});

このコードはメインスレッドで実行され、画像 URL をウェブ ワーカーに送信するようにフォームを設定します。そこから、ウェブ ワーカーコードは外部 exif-reader スクリプトを読み込む importScripts ステートメントで始まり、メインスレッドへのメッセージ パイプラインを設定します。

// exif-worker.js

// Import the exif-reader script:
importScripts('/js/with-worker/exifreader.js');

// Set up a messaging pipeline to send the Exif data to the `window`:
self.addEventListener('message', ({ data }) => {
  getExifDataFromImage(data).then(status => {
    self.postMessage(status);
  });
});

この JavaScript は、ユーザーが JPEG ファイルの URL を含むフォームを送信したときに、その URL がウェブ ワーカーに届くようにメッセージ パイプラインを設定します。次のコードは、JPEG ファイルから Exif メタデータを抽出し、HTML 文字列を構築して、その HTML を window に送り返し、最終的にユーザーに表示します。

// Takes a blob to transform the image data into an `ArrayBuffer`:
// NOTE: these promises are simplified for readability, and don't include
// rejections on failures. Check out the complete web worker code:
// https://chrome.dev/learn-performance-exif-worker/js/with-worker/exif-worker.js
const readBlobAsArrayBuffer = blob => new Promise(resolve => {
  const reader = new FileReader();

  reader.onload = () => {
    resolve(reader.result);
  };

  reader.readAsArrayBuffer(blob);
});

// Takes the Exif metadata and converts it to a markup string to
// display in the Exif metadata viewer in the DOM:
const exifToMarkup = exif => Object.entries(exif).map(([exifNode, exifData]) => {
  return `
    <details>
      <summary>
        <h2>${exifNode}</h2>
      </summary>
      <p>${exifNode === 'base64' ? `<img src="data:image/jpeg;base64,${exifData}">` : typeof exifData.value === 'undefined' ? exifData : exifData.description || exifData.value}</p>
    </details>
  `;
}).join('');

// Fetches a partial image and gets its Exif data
const getExifDataFromImage = imageUrl => new Promise(resolve => {
  fetch(imageUrl, {
    headers: {
      // Use a range request to only download the first 64 KiB of an image.
      // This ensures bandwidth isn't wasted by downloading what may be a huge
      // JPEG file when all that's needed is the metadata.
      'Range': `bytes=0-${2 ** 10 * 64}`
    }
  }).then(response => {
    if (response.ok) {
      return response.clone().blob();
    }
  }).then(responseBlob => {
    readBlobAsArrayBuffer(responseBlob).then(arrayBuffer => {
      const tags = ExifReader.load(arrayBuffer, {
        expanded: true
      });

      resolve({
        status: true,
        message: Object.values(tags).map(tag => exifToMarkup(tag)).join('')
      });
    });
  });
});

少し長いですが、ウェブ ワーカーのかなり複雑なユースケースでもあります。ただし、結果は作業に見合うものであり、このユースケースに限定されるものではありません。ウェブ ワーカーは、fetch 呼び出しの分離とレスポンスの処理、メインスレッドをブロックせずに大量のデータを処理するなど、さまざまな用途に使用できます。

ウェブ アプリケーションのパフォーマンスを改善する場合は、ウェブ ワーカー コンテキストで合理的に実行できることを検討することから始めます。この改善は大きな効果をもたらし、ウェブサイトの全体的なユーザー エクスペリエンスの向上につながります。