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

前のモジュールでは、ウェブワーカーの概要について学習しました。ウェブワーカーは、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. この画像リンクをフィールドに貼り付け、[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 呼び出しの分離やレスポンスの処理、メインスレッドをブロックせずに大量のデータを処理するなど、あらゆる用途に使用できます。これらは開始条件にすぎません。

ウェブ アプリケーションのパフォーマンスを向上させるときは、ウェブ ワーカーのコンテキストで合理的に行えることをすべて検討します。これによって大幅な改善が見られ、ウェブサイトの全体的なユーザー エクスペリエンスの向上につながります。