高效加载 WebAssembly 模块

在使用 WebAssembly 时,您通常希望下载模块、编译模块、实例化模块,然后在 JavaScript 中使用其导出的任何内容。这篇文章首先介绍了一个常见但欠佳的代码片段来完成这项工作,讨论了几种可能的优化方法,并最终展示了从 JavaScript 运行 WebAssembly 的最简单、最有效的方法。

在使用 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 模块。由于下载和编译并行发生,因此速度更快 - 特别是对于大型有效负载。

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.

要启用此优化,请使用 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,则可以直接传递 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);
})();

不过,我个人觉得将其放在单独的一行更易读。

看看我们如何将响应编译成模块,然后立即实例化它?事实证明,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!