網頁應用程式的 WebAssembly 效能模式

本指南旨在協助網頁程式開發人員,充分發揮 WebAssembly 的效益。 您會學到如何使用 Wasm 外包需耗用大量 CPU 資源的工作 也可以看到跑步範例本指南涵蓋所有內容,包括 載入 Wasm 模組以最佳化編譯和例項化作業。這項服務 進一步討論將耗用大量 CPU 的工作轉移到網路工作站,並深入研究 建立網路時需要的導入決策 工作人員,以及是否要永久保存或在需要時啟動。 引導疊代開發方法,並引進一種效能模式 直到能提供問題的最佳解決方案

假設

假設您有一項需要耗用大量 CPU 資源的工作 WebAssembly (Wasm) 能提供近乎原生的效能。耗用大量 CPU 的工作 這個數字是計算數字的階乘。 階乘是整數及其下所有整數的乘積。適用對象 例如,四的階乘 (以 4! 寫成) 等於 24 (也就是 4 * 3 * 2 * 1)。數據即將大幅增加,例如,16!2,004,189,184。更逼真的例子是以耗用大量 CPU 的工作為例 掃描條碼追蹤光柵圖片

執行 factorial() 的疊代 (而非遞迴) 實作 函式如下列以 C++ 編寫的程式碼範例所示。

#include <stdint.h>

extern "C" {

// Calculates the factorial of a non-negative integer n.
uint64_t factorial(unsigned int n) {
    uint64_t result = 1;
    for (unsigned int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

}

至於文章的其他部分,假設有一個以編譯為基礎的 Wasm 模組 這個 factorial() 函式含有 Emscripten,且該檔案位於名為 factorial.wasm 的檔案中 使用所有 程式碼最佳化最佳做法。 如需複習做法,請參閱 使用 ccall/cwrap 從 JavaScript 呼叫已編譯的 C 函式。 下列指令的用途是將 factorial.wasm 編譯為 獨立 Wasm

emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]'  --no-entry

HTML 中有 form,其中包含 inputoutput 配對,且提交了 button。這些元素是根據名稱從 JavaScript 參照而來。

<form>
  <label>The factorial of <input type="text" value="12" /></label> is
  <output>479001600</output>.
  <button type="submit">Calculate</button>
</form>
const input = document.querySelector('input');
const output = document.querySelector('output');
const button = document.querySelector('button');

載入、編譯模組並執行個體化

您必須先載入 Wasm 模組,才能使用 Wasm 模組。在網頁上 透過 fetch()敬上 也能使用 Google Cloud CLI 或 Compute Engine API如您所知,您的網頁應用程式依附於 會耗用大量 CPU 的工作,您應該盡早預先載入 Wasm 檔案。個人中心 如何使用 支援 CORS 的擷取功能 應用程式的 <head> 部分。

<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />

實際上,fetch() API 為非同步性質,因此您需要 await 結果。

fetch('factorial.wasm');

接下來,編譯並例項化 Wasm 模組。其中一個名稱是 呼叫函式 WebAssembly.compile()敬上 (加號 WebAssembly.compileStreaming()) 和 WebAssembly.instantiate() 設定這類工作所需的值 WebAssembly.instantiateStreaming() 方法會編譯並且直接從串流的 Wasm 模組執行個體化 基礎來源,例如 fetch()—不需要 await。這是最有效率的 以及載入 Wasm 程式碼的最佳方式假設 Wasm 模組將 factorial() 函式,然後可以直接使用該函式。

const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);
const factorial = resultObject.instance.exports.factorial;

button.addEventListener('click', (e) => {
  e.preventDefault();
  output.textContent = factorial(parseInt(input.value, 10));
});

將工作移交給網路工作處理人員

如果您在主執行緒上執行這項作業,而進行了會耗用大量 CPU 的工作,將面臨風險 因而封鎖整個應用程式常見做法是將這類工作轉移到網路 Worker。

重整主執行緒的結構

如要將耗用大量 CPU 的工作移至網路工作站,第一步是重組 應用程式主要執行緒現在會建立 Worker,除此之外 只能處理將輸入內容傳送至網路工作站,然後才會收到 輸出及顯示

/* Main thread. */

let worker = null;

// When the button is clicked, submit the input value
//  to the Web Worker.
button.addEventListener('click', (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker('worker.js');

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({ integer: parseInt(input.value, 10) });
});

不佳:工作在 Web Worker 中執行,但程式碼有缺陷

Web Worker 會將 Wasm 模組例項化,並在收到訊息時 會執行大量 CPU 的工作,並將結果傳回主要執行緒。 這種方法發生問題是 將 Wasm 模組執行個體化 WebAssembly.instantiateStreaming() 是一種非同步作業。也就是說 但程式碼是必須的最糟的情況是,主執行緒會在 Web Worker 尚未準備就緒,Web Worker 從未收到訊息。

/* Worker thread. */

// Instantiate the Wasm module.
// 🚫 This code is racy! If a message comes in while
// the promise is still being awaited, it's lost.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);
const factorial = resultObject.instance.exports.factorial;

// Listen for incoming messages, run the task,
// and post the result.
self.addEventListener('message', (e) => {
  const { integer } = e.data;
  self.postMessage({ result: factorial(integer) });
});

更好的:工作是在 Web Worker 中執行,但可能有冗餘的載入和編譯功能

如要解決非同步 Wasm 模組例項的問題, 將 Wasm 模組的載入、編譯和例項化作業全部移至事件中 但這就代表您必須在每次 已收到的訊息。透過 HTTP 快取和 HTTP 快取 編譯的 Wasm 位元碼,這不是最糟糕的解決方案 。

將非同步程式碼移到 Web Worker 的開頭而非 其實等待承諾來到您實際完成 變數,程式會立即移至 程式碼,也不會遺失主執行緒中的任何訊息。活動內容 就表示保證可以等待。

/* Worker thread. */

const importObject = {};
// Instantiate the Wasm module.
// 🚫 If the `Worker` is spun up frequently, the loading
// compiling, and instantiating work will happen every time.
const wasmPromise = WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);

// Listen for incoming messages
self.addEventListener('message', async (e) => {
  const { integer } = e.data;
  const resultObject = await wasmPromise;
  const factorial = resultObject.instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({ result });
});

良好:工作在 Web Worker 中執行,且僅載入及編譯一次

靜態函數的結果 WebAssembly.compileStreaming()敬上 是會解析為 WebAssembly.Module。 這個物件另一個好用的功能是 postMessage()。 這表示 Wasm 模組只能在主程式中載入並編譯一次 執行緒 (或其他 Web Worker 只關心載入和編譯的問題), 再轉移到負責部分 CPU 的 Web Worker 工作。以下程式碼顯示這個流程。

/* Main thread. */

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

let worker = null;

// When the button is clicked, submit the input value
// and the Wasm module to the Web Worker.
button.addEventListener('click', async (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker('worker.js');

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

在 Web Worker 上,剩下的就是擷取 WebAssembly.Module 物件並將其執行個體化由於與 WebAssembly.Module 的訊息並非 串流後,Web Worker 中的程式碼現在會使用 WebAssembly.instantiate() 而不是先前的 instantiateStreaming() 變數建立例項 模組是快取在變數中,因此執行個體化工作只需 進入了網路工作站之後

/* Worker thread. */

let instance = null;

// Listen for incoming messages
self.addEventListener('message', async (e) => {
  // Extract the `WebAssembly.Module` from the message.
  const { integer, module } = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via `postMessage()`.
  instance = instance || (await WebAssembly.instantiate(module, importObject));
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({ result });
});

完美:工作在內嵌 Web Worker 中執行,且僅載入和編譯一次

就算使用 HTTP 快取,仍然能獲得 (理想) 快取 Web Worker 程式碼和 觸及聯播網的費用高昂常見的效能秘訣是 內嵌 Web Worker,並以 blob: 網址載入。這項操作仍需要 將編譯的 Wasm 模組傳遞給網路工作處理程式進行例項化,如 Web Worker 和主執行緒的結構定義並不相同 建立 3D 模型

/* Main thread. */

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

let worker = null;

const blobURL = URL.createObjectURL(
  new Blob(
    [
      `
let instance = null;

self.addEventListener('message', async (e) => {
  // Extract the \`WebAssembly.Module\` from the message.
  const {integer, module} = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via \`postMessage()\`.
  instance = instance || await WebAssembly.instantiate(module, importObject);
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({result});
});
`,
    ],
    { type: 'text/javascript' },
  ),
);

button.addEventListener('click', async (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker(blobURL);

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

延遲或熱切地建立 Web Worker

到目前為止,所有 Web Worker 都會延遲地啟動所有程式碼範例,也就是 。視您的應用程式而定 更迅速地建立 Web Worker,例如當應用程式處於閒置狀態, 是應用程式自行啟動程序的一部分因此,移動網路工作站 新增至按鈕的事件監聽器外的程式碼。

const worker = new Worker(blobURL);

// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
  output.textContent = e.result;
});

讓網路工作處理人員保持正常運作

你可能會想問,自己是否該保留 Web Worker 或視需要重新建立這兩種做法 並用自己的優勢和缺點例如維護 永久性的工作站可能會增加應用程式的記憶體用量, 較難處理並行工作,因為您必須設法對應結果 從 Web Worker 傳回要求。另一方面 worker 的啟動程式碼可能十分複雜,因此 就會增加負載幸好,你可以採取這種做法 進行評估 User Timing API

到目前為止,程式碼範例都保留了一個永久網路工作站。下列 程式碼範例會在需要時建立新的 Web Worker。請注意,您需要 追蹤 終止網路工作處理程序 你自己。(程式碼片段會略過錯誤處理,但以防萬一 無論在任何情況下,都請務必終止。

/* Main thread. */

let worker = null;

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

const blobURL = URL.createObjectURL(
  new Blob(
    [
      `
// Caching the instance means you can switch between
// throw-away and permanent Web Worker freely.
let instance = null;

self.addEventListener('message', async (e) => {
  // Extract the \`WebAssembly.Module\` from the message.
  const {integer, module} = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via \`postMessage()\`.
  instance = instance || await WebAssembly.instantiate(module, importObject);
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({result});
});  
`,
    ],
    { type: 'text/javascript' },
  ),
);

button.addEventListener('click', async (e) => {
  e.preventDefault();
  // Terminate a potentially running Web Worker.
  if (worker) {
    worker.terminate();
  }
  // Create the Web Worker lazily on-demand.
  worker = new Worker(blobURL);
  worker.addEventListener('message', (e) => {
    worker.terminate();
    worker = null;
    output.textContent = e.data.result;
  });
  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

示範

你可以試用兩個示範模式。一個包含 臨時 Web Worker (原始碼) 另一個則是 永久網路工作處理人員 (原始碼)。 如果您開啟 Chrome 開發人員工具並查看控制台,就會看到「使用者」 計時 API 記錄,以測量從按鈕點擊到 也就是畫面上顯示的結果「Network」分頁會顯示 blob: 網址 請求。在本例中,臨時和永久性的時間差異 約 3 倍。實際上,對人類的眼睛看法 確認是否屬於此情況這與您為現實生活的應用程式所提供的結果可能大不相同。

具有臨時工作站的 Factorial Wasm 試用版應用程式。Chrome 開發人員工具已開啟。其中有兩個 blob:「Network」分頁中的網址要求,「Console」會顯示兩個計算時間。

具有永久工作站的 Factorial Wasm 試用版應用程式。Chrome 開發人員工具已開啟。只有一個 blob:「Network」分頁中的網址要求,「Console」會顯示四個計算時間。

結論

這篇文章探討了與 Wasm 處理的一些效能模式。

  • 一般來說,建議優先選擇串流方法 (WebAssembly.compileStreaming()WebAssembly.instantiateStreaming()) 自己的非串流影片 (WebAssembly.compile()WebAssembly.instantiate())。
  • 可以的話,在網路工作處理人員中將高效能的工作外包給外包,然後執行 Wasm 載入及編譯作業只在網路工作處理程序之外執行一次。如此一來 Web Worker 只需要從主要伺服器接收的 Wasm 模組例項化 發生載入和編譯作業的執行緒 WebAssembly.instantiate(),表示執行個體可以在您的物件中快取 讓 Web Worker 永遠有效
  • 仔細評估是否有必要保留一個永久網路工作者 或是在需要時建立臨時的網路工作人員其他 我們認為建立網路工作處理序的最佳時機就是您。注意事項 考慮因素包括記憶體用量、Web Worker 例項化時長 但可能必須處理並行要求的複雜度

一旦將這些模式納入考量,就代表您正朝著正確的方向邁進 Wasm 效能。

特別銘謝

本指南由 Andreas Haas Jakob Kummerow Deepti GandluriAlon ZakaiFrancis McCabeFrançois BeaufortRachel Andrew