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

前のモジュールでは、ウェブワーカーの概要について学習しました。ウェブワーカーは、JavaScript をメインスレッドから別のウェブワーカー スレッドに移動することで入力の応答性を改善できます。これにより、メインスレッドに直接アクセスする必要がない処理がある場合に、ウェブサイトの Interaction to Next Paint(INP)を改善できます。ただし、概要だけでは不十分です。このモジュールでは、ウェブワーカーの具体的なユースケースについて説明します。

そのようなユースケースの 1 つが、画像から Exif メタデータを削除する必要があるウェブサイトです。これは、あまり知られていないコンセプトではありません。実際、Flickr のようなウェブサイトでは、Exif メタデータを表示して、色深度、カメラのメーカー、モデルなど、ホストする画像の技術的な詳細を知ることができます。

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

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

まず、ウェブワーカーを使用せずにこの作業を行う場合、メインスレッドがどのように表示されるかを確認します。手順は次のとおりです。

  1. Chrome で新しいタブを開き、DevTools を開きます。
  2. パフォーマンス パネルを開きます。
  3. https://exif-worker.glitch.me/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://exif-worker.glitch.me/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://glitch.com/edit/#!/exif-worker?path=js%2Fwith-worker%2Fexif-worker.js%3A10%3A5
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 呼び出しの分離やレスポンスの処理、メインスレッドをブロックせずに大量のデータを処理するなど、あらゆることに使用できます。これは開始条件にすぎません。

ウェブ アプリケーションのパフォーマンスを向上させる場合は、ウェブワーカーのコンテキストで合理的に実行できることを検討します。こうしたメリットは大きく、ウェブサイトの全体的なユーザー エクスペリエンスの向上につながる可能性があります。