Como carregar módulos WebAssembly com eficiência

Ao trabalhar com o WebAssembly, é comum querer baixar um módulo, compilá-lo, instanciá-lo e, em seguida, usar o que for exportado em JavaScript. Nesta postagem, explicamos nossa abordagem recomendada para aumentar a eficiência.

Ao trabalhar com o WebAssembly, normalmente é recomendável fazer o download de um módulo, compilá-lo, instanciá-lo e usar o que for exportado em JavaScript. Esta postagem começa com um snippet de código comum, mas abaixo do ideal, que faz exatamente isso, discute várias otimizações possíveis e, por fim, mostra a forma mais simples e eficiente de executar o WebAssembly no JavaScript.

Esse snippet de código faz todo o processo download-compilar-instanciar, embora de uma forma abaixo do ideal:

Não use!

(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é que seja concluída. 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 abordar isso em breve.

Quase todas as operações no snippet modificado agora são assíncronas, já que 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 manter a consistência e manter a linha de execução principal livre, podemos usar a WebAssembly.instantiate(module) assíncrona.

(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 do compile que sugerimos anteriormente. Com a compilação de streaming, o navegador já pode começar a compilar o módulo WebAssembly enquanto o download dos bytes do módulo ainda está em andamento. Como o download e a compilação acontecem em paralelo, esse processo é mais rápido, especialmente 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 eliminar o buffer da matriz intermediário, já que agora podemos transmitir diretamente a instância Response retornada por await fetch(url).

(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 em uma instância Response. Se você não precisar de response em outro lugar no seu código, transmita a promessa retornada por fetch diretamente, sem gerar await de forma explícita:

(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ê também não precisar do resultado de 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);
})();

Pessoalmente, acho mais legível mantê-lo em uma linha separada.

Viu como compilamos a resposta em um módulo e a instanciamos imediatamente? Como resultado, WebAssembly.instantiate pode compilar e instanciar de uma só vez. A API WebAssembly.instantiateStreaming faz isso de maneira 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 única 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 que aplicamos podem ser resumidas da seguinte forma:

  • Usar APIs assíncronas para evitar o bloqueio da linha de execução principal
  • Usar APIs de streaming para compilar e instanciar módulos WebAssembly mais rapidamente
  • Não escreva códigos desnecessários

Divirta-se com o WebAssembly!