具體的網路工作人員用途

在最後一個單元中,我們概略介紹了 Web Worker。您可以將 JavaScript 從主執行緒移至獨立的 Web Worker 執行緒,藉此改善輸入內容的回應速度,進而提升網站的「與下一次算繪的互動 (INP)」指標。如果工作不需要直接存取主執行緒,Web Worker 就很實用。不過,光是概覽還不夠,本單元將提供網頁工作站的具體用途。

舉例來說,網站可能需要從圖片中移除 Exif 中繼資料,這並非天方夜譚。事實上,Flickr 等網站會提供 Exif 中繼資料供使用者查看,瞭解所託管圖片的技術詳細資料,例如色深、相機廠牌和型號,以及其他資料。

不過,如果完全在主執行緒上執行,擷取圖片、將圖片轉換為 ArrayBuffer,以及擷取 Exif 中繼資料的邏輯可能會相當耗費資源。幸好,網頁工作人員範圍允許這項工作在主執行緒外完成。接著,透過 Web Worker 的訊息傳輸管道,Exif 中繼資料會以 HTML 字串的形式傳輸回主執行緒,並顯示給使用者。

沒有網頁工作站時的主執行緒

首先,請觀察在沒有網頁工作人員的情況下執行這項工作時,主執行緒的樣貌。如要這樣做,請按照下列步驟操作:

  1. 在 Chrome 中開啟新分頁,並開啟開發人員工具。
  2. 開啟效能面板
  3. 前往 https://chrome.dev/learn-performance-exif-worker/without-worker.html
  4. 在效能面板中,按一下開發人員工具窗格右上角的「記錄」
  5. 這個圖片連結 (或您選擇的其他含有 Exif 中繼資料的連結) 貼到欄位中,然後按一下「Get that JPEG!」(取得該 JPEG!) 按鈕。
  6. 介面填入 Exif 中繼資料後,請再次點選「記錄」停止記錄。
效能剖析器顯示圖片中繼資料擷取器應用程式的活動,完全發生在主要執行緒上。其中有兩項重要的長時間工作:一項是執行擷取作業來取得並解碼所要求的圖片,另一項則是從圖片中擷取中繼資料。
圖片中繼資料擷取器應用程式中的主要執行緒活動。請注意,所有活動都會在主要執行緒中進行。

請注意,除了可能存在的其他執行緒 (例如點陣化執行緒等),應用程式中的所有項目都會在主執行緒上發生。在主要執行緒中,會發生下列情況:

  1. 表單會接收輸入內容,並傳送 fetch 要求,取得含有 Exif 中繼資料的圖片初始部分。
  2. 圖片資料會轉換為 ArrayBuffer
  3. exif-reader 指令碼用於從圖片擷取 Exif 中繼資料。
  4. 系統會擷取中繼資料來建構 HTML 字串,然後填入中繼資料檢視器。

現在,請比較相同行為的實作方式,但使用 Web Worker!

主執行緒搭配網頁工作站的樣子

現在您已瞭解如何從主要執行緒的 JPEG 檔案中擷取 Exif 中繼資料,接下來請看看加入網頁工作人員後的情況:

  1. 在 Chrome 中開啟另一個分頁,然後開啟該分頁的開發人員工具。
  2. 開啟效能面板
  3. 前往 https://chrome.dev/learn-performance-exif-worker/with-worker.html
  4. 在「成效」面板中,按一下開發人員工具窗格右上角的錄製按鈕
  5. 這個圖片連結貼到欄位中,然後按一下「Get that JPEG!」按鈕。
  6. 介面填入 Exif 中繼資料後,請再次點選錄製按鈕停止錄製。
效能分析器顯示圖片中繼資料擷取器應用程式在主要執行緒和網頁工作站執行緒上發生的活動。雖然主要執行緒上仍有長時間執行的工作,但時間已大幅縮短,圖片擷取/解碼和中繼資料擷取作業完全在網頁背景工作執行緒上進行。主執行緒唯一的工作是將資料傳遞給網頁工作站,以及從網頁工作站傳遞資料。
圖片中繼資料擷取器應用程式中的主要執行緒活動。請注意,大部分工作都是在額外的網頁工作執行緒中完成。

這就是 Web Worker 的強大之處。與其在主執行緒上執行所有作業,不如在獨立執行緒上執行所有作業,但使用 HTML 填入中繼資料檢視器除外。這表示主執行緒已釋出,可執行其他工作。

與未使用 Web Worker 的應用程式版本不同,exif-reader 指令碼不會載入主執行緒,而是載入 Web Worker 執行緒,這或許是最大的優點。也就是說,下載、剖析及編譯 exif-reader 指令碼的成本不會計入主執行緒。

現在來深入瞭解網路工作人員程式碼,瞭解如何實現這一切!

查看 Web Worker 程式碼

光是瞭解網頁工作人員帶來的差異還不夠,至少在這個案例中,您也需要瞭解程式碼的樣貌,才能知道網頁工作人員範圍內有哪些可能性。

首先,請使用必須在網頁背景工作進入畫面之前發生的主執行緒程式碼:

// 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';
});

這段程式碼會在主執行緒上執行,並設定表單,將圖片網址傳送至 Web Worker。從這裡開始,Web Worker 程式碼會先使用 importScripts 陳述式載入外部 exif-reader 指令碼,然後設定與主執行緒的訊息傳輸管道:

// 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 檔案的網址時,該網址就會傳送至 Web Worker。接下來,這段程式碼會從 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 呼叫和處理回應、處理大量資料而不封鎖主要執行緒,這只是其中一小部分。

改善網頁應用程式效能時,請先思考網頁工作人員環境中可合理執行的任何作業。這項改善可能帶來顯著效益,並提升網站的整體使用者體驗。