有效率地載入 WebAssembly 模組

使用 WebAssembly 時,您通常會想要下載、編譯模組、將其例項化,然後使用 JavaScript 匯出的任何模組。本文將說明最佳化效率的建議做法。

馬蒂亞斯拜恩斯
Mathias Bynens

使用 WebAssembly 時,您通常會想要下載、編譯模組、將其例項化,然後使用 JavaScript 匯出的任何項目。本文將從完全不太理想的程式碼片段開始,首先探討了幾種可能的最佳化做法,最後則顯示透過 JavaScript 執行 WebAssembly 最簡單、最有效率的方法。

以下程式碼片段執行完整的下載編譯反抗性舞蹈,儘管有不盡理想的情況:

請勿使用此屬性!

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

請注意,我們如何使用 new WebAssembly.Module(buffer) 將回應緩衝區轉換為模組。這是同步 API,因此會封鎖主執行緒,直到完成為止。為避免使用,Chrome 會針對大小超過 4 KB 的緩衝區停用 WebAssembly.Module。如要解決大小限制,可以改用 await WebAssembly.compile(buffer)

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

await WebAssembly.compile(buffer) 「仍然」不是最佳方法,但我們稍後就會說明。

由於使用 await 會很清楚,因為修改後的程式碼片段中幾乎所有作業現在都是非同步。唯一的例外狀況是 new WebAssembly.Instance(module),也就是與 Chrome 相同的 4 KB 緩衝區空間限制。為保持一致性,並達到不讓主執行緒自由的情形,我們可以使用非同步的 WebAssembly.instantiate(module)

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

再回到我稍早提到的 compile 最佳化作業。透過串流編譯,瀏覽器可以在系統仍在下載模組位元組的情況下,開始編譯 WebAssembly 模組。由於下載和編譯作業是同時進行,因此速度會更快,尤其是針對大型酬載。

如果下載時間超過 WebAssembly 模組的編譯時間,WebAssembly.compileStreaming() 就會在最後一個位元組下載完畢後立即完成編譯。

如要啟用這項最佳化功能,請使用 WebAssembly.compileStreaming,而非 WebAssembly.compile。這項變更也可讓我們去除中繼陣列緩衝區,因為現在可直接傳遞 await fetch(url) 傳回的 Response 例項。

(async () => {
  const response = await fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(response);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

WebAssembly.compileStreaming API 也接受一個承諾會解析為 Response 執行個體。如果您不需要在程式碼的其他位置使用 response,可以直接傳遞 fetch 傳回的承諾,且不必明確 await 結果:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(fetchPromise);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

如果其他地方不需要 fetch 結果,甚至可以直接傳遞:

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

不過,我個人覺得很容易理解,因此請分行列出。

我們如何把回應編譯成模組,然後立即將其例項化?後,WebAssembly.instantiate 可以一次編譯並例項化。WebAssembly.instantiateStreaming API 會以串流的方式執行這項作業:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // To create a new instance later:
  const otherInstance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

如果只需要一個執行個體,就沒有理由保留 module 物件,進一步簡化程式碼:

// This is our recommended way of loading WebAssembly.
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

我們套用的最佳化建議總結如下:

  • 使用非同步 API 避免封鎖主執行緒
  • 使用串流 API 更快編譯 WebAssembly 模組並例項化
  • 不要撰寫不需要的程式碼

享受 WebAssembly 帶來的樂趣!