Como carregar módulos WebAssembly com eficiência

Ao trabalhar com o WebAssembly, você geralmente quer fazer o download de um módulo, compilá-lo, instanciar e usar o que ele exporta em JavaScript. Esta postagem explica nossa abordagem recomendada para eficiência ideal.

Ao trabalhar com o WebAssembly, muitas vezes você quer fazer o download de um módulo, compilá-lo, instanciar e usar o que ele exporta em JavaScript. Esta postagem começa com um trecho de código comum, mas não ideal, fazendo exatamente isso, discute várias otimizações possíveis e, por fim, mostra a maneira mais simples e eficiente de executar o WebAssembly no JavaScript.

Este snippet de código faz a dança completa de download-compilação-instanciação, embora de maneira não ideal:

Não use isso!

(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);
})();

Observe como usamos new WebAssembly.Module(buffer) para transformar um buffer de resposta em um módulo. Essa é uma API síncrona, o que significa que ela bloqueia a linha de execução principal até a conclusão. Para desencorajar o uso, o Chrome desativa WebAssembly.Module para buffers maiores que 4 KB. Para contornar o limite de tamanho, podemos usar 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) ainda não é a abordagem ideal, mas vamos chegar lá em um segundo.

Quase todas as operações no snippet modificado agora são assíncronas, como o uso de await deixa claro. A única exceção é new WebAssembly.Instance(module), que tem a mesma restrição de tamanho de buffer de 4 KB no Chrome. Para consistência e para manter a linha de execução principal livre, podemos usar o WebAssembly.instantiate(module) assíncrono.

(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);
})();

Vamos voltar à otimização de compile que mencionei anteriormente. Com a compilação em streaming, o navegador já pode começar a compilar o módulo WebAssembly enquanto os bytes do módulo ainda estão sendo transferidos. Como o download e a compilação acontecem em paralelo, isso é mais rápido, principalmente para payloads grandes.

Quando o tempo de download é
maior que o tempo de compilação do módulo do WebAssembly, o WebAssembly.compileStreaming()
conclui a compilação quase imediatamente após o download dos últimos bytes.

Para ativar essa otimização, use WebAssembly.compileStreaming em vez de WebAssembly.compile. Essa mudança também permite que nos livremos do buffer de matriz intermediário, já que agora podemos transmitir a instância Response retornada por await fetch(url) diretamente.

(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);
})();

A API WebAssembly.compileStreaming também aceita uma promessa que é resolvida para uma instância Response. Se você não precisar de response em outro lugar do código, transmita diretamente a promessa retornada por fetch, sem await o resultado explicitamente:

(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);
})();

Se você não precisar do resultado fetch em nenhum outro lugar, poderá até mesmo transmiti-lo diretamente:

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

Pessoalmente, acho mais fácil ler quando está em uma linha separada.

Percebeu como compilamos a resposta em um módulo e a instanciamos imediatamente? O WebAssembly.instantiate pode compilar e instanciar de uma só vez. A API WebAssembly.instantiateStreaming faz isso de maneira contínua:

(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);
})();

Se você precisar apenas de uma instância, não há necessidade de manter o objeto module, simplificando ainda mais o código:

// 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);
})();

As otimizações aplicadas podem ser resumidas da seguinte maneira:

  • Usar APIs assíncronas para evitar o bloqueio da linha de execução principal
  • Usar APIs de streaming para compilar e instanciar módulos do WebAssembly com mais rapidez
  • Não escreva código que você não precisa

Divirta-se com o WebAssembly!