高效加载 WebAssembly 模块

使用 WebAssembly 时,您经常需要下载一个模块,对其进行编译,将其实例化,然后使用以 JavaScript 导出的任何内容。此博文介绍了我们推荐的有助于实现最佳效率的方法。

Mathias Bynens
Mathias Bynens

使用 WebAssembly 时,您常常需要下载模块、编译模块、实例化模块,以及 然后使用它以 JavaScript 格式导出的任何内容这篇博文的开头有一个常见但不太理想的代码 代码段,讨论了几种可能的优化措施,并最终展示了 从 JavaScript 运行 WebAssembly 最简单、最高效的方法。

以下代码段完成了完整的“download-compile-instantiate”舞蹈,不过,效果欠佳:

请勿使用!

(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,这意味着它会阻塞主线程,直至其完成。为了防止使用它 对大于 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),它具有相同的 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 的 promise 实例。如果您在代码中的其他位置不需要 response,则可以将 promise 由 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!