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-compile-instantiate, embora de uma forma abaixo do 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 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 nos permite nos livrar 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 no código, poderá transmitir a promessa retornada por fetch diretamente, sem await explicitamente o resultado:

(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 outro lugar, poderá até 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);
})();

No entanto, pessoalmente, acho mais legível mantê-lo 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 de streaming:

(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ê só precisa de uma instância, não faz sentido 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!