谷歌:如何以最高效的方式載入 WebAssembly

西樓聽雨發表於2018-04-14
原文標題:Loading WebAssembly modules efficiently

原文連結:https://developers.google.com/web/updates/2018/04/loading-wasm(需越牆)

 原文作者:Mathias Bynens 譯者:西樓聽雨 (轉載請註明出處) 


我們在使用 WebAssembly 的時候,通常的做法都是,先下載一個模組,然後編譯它,再進行例項化,最後使用通過 JavaScript 匯出(exports)的東西。本文將以一段常見的但不是最優的實現這種做法的程式碼來開始,接著再討論幾個可以優化的地方,最後以給出一種最簡單最高效的方式來結束。

提示:像 Emsciptent 這類工具,可以自動幫你生成這種做法的模板程式碼,所以你沒必要自己動手編寫。本文的目的是考慮到你可能會有需要對 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) 來把一個 response 的 buffer 來轉化為一個 module 的。不過這個 API 是同步的,這就意味著在它執行完之前它會一直阻塞主執行緒。為了抑制對它的使用,Chrome 會在 buffer 的大小超過 4KB 時禁止使用它。如果要避開這個限制,我們可以改為使用 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 的“4KB buffer 大小”的限制。為了保持一致性以及“保障主執行緒任何時候都不受牽制”的目的,我們可以改為使用非同步的 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 的最優的方法。藉助於“流式編譯(streaming compilation)”,瀏覽器現在已經可以直接在模組資料還在下載時就開始編譯 WebAssembly 模組。由於下載和編譯是同時進行的,速度自然更快——特別是在載荷(payload)大的時候(譯:即模組的體積大的時候)。

When the download time is
longer than the compilation time of the WebAssembly module, then WebAssembly.compileStreaming()
finishes compilation almost immediately after the last bytes are downloaded.

要使用這種優化,我們需要改 WebAssebly.compile 的使用為WebAssembly.compileStreaming。這種改變還可以幫我們避免中間性的 arraybuffer,因為現在我們傳遞的直接是 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);
})();複製程式碼
注意:這種方式要求伺服器必須對 .wasm 檔案做正確的 MIME 型別的配置,方法是傳送Content-Type: application/wasm 頭。在上一個例子中,這個步驟不是必須的,因為我們傳遞的是 response 的 arraybuffer,所以就不會發生對 MIME 型別的檢測。

WebAssembly.compileStreaming API 還支援傳入能夠解析(resolve)為 Response 的 promise。如果你在程式碼中沒有其他使用response的地方,這樣你就可以直接傳遞fetch返回的promise,不需要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);
})();
複製程式碼

雖然如此,不過我個人覺得把它單獨放一行更具可讀性。

看到我們是如何把 response 編譯為一個 module,又是如何把它立刻例項化的了嗎?其實,WebAssembly.instantiate 可以一步到位完成到編譯和例項化。WebAssembly.instantiateStreaming API 當然也可以,而且是流式的:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // 稍後建立的另一個新的例項:
  const otherInstance = await WebAssembly.instantiate(module); 
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();
複製程式碼

如果你只需要一個例項的話,儲存 module 物件就沒有任何意義了,所以程式碼還可以更一步簡化:

// 這就是我們所推薦的載入 WebAssembley 的方式
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();
複製程式碼

你可以在 WebAssembly Studio 中線上把玩這段程式碼示例

總結以下我們所應用的優化:

  • 使用非同步 API 來防止主執行緒阻塞
  • 使用流式 API 來更快地編譯和例項化 WebAssembly 模組
  • 不寫不需要程式碼

祝你玩 WebAssembly 玩的開心!


相關文章