Como carregar módulos WebAssembly com eficiência

Ao trabalhar com o WebAssembly, é comum fazer o download de um módulo, compilá-lo, instanciá-lo e usar o que ele exportar em JavaScript. Esta postagem explica nossa abordagem recomendada para alcançar a eficiência máxima.

Ao trabalhar com o WebAssembly, é comum fazer o download de um módulo, compilá-lo, instanciá-lo e e usar o que ele exportar em JavaScript. Esta publicação começa com um código comum, mas abaixo do ideal snippet que faz exatamente isso, discute várias otimizações possíveis e, por fim, mostra os maneira mais simples e eficiente de executar o WebAssembly em 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. Esta é uma síncrona, o que significa que ela bloqueia a linha de execução principal até que seja concluída. Para desencorajar seu uso, o Chrome desativa WebAssembly.Module para buffers maiores que 4 KB. Para contornar o limite de tamanho, podemos use 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 um segundo.

Quase todas as operações no snippet modificado agora são assíncronas, já que o uso de await facilita claras. A única exceção é new WebAssembly.Instance(module), que tem o mesmo buffer de 4 KB restrição de tamanho no Chrome. Para consistência e a fim de manter a linha de execução principal sem custo financeiro, podemos usar o modelo 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);
})();

Vamos voltar à otimização do compile que sugerimos antes. Com streaming compilação, o navegador já pode começará a compilar o módulo WebAssembly durante o download dos bytes do módulo. Desde o download e a compilação acontecem em paralelo, isso é mais rápido — especialmente para payloads grandes.

Quando o tempo de download for
maior que o tempo de compilação do módulo WebAssembly, então WebAssembly.compileStreaming()
finaliza 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 alteração também nos permite eliminar o buffer intermediário da matriz, já que agora podemos passar o Instância de Response retornada diretamente 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 como Response. instância. Se você não precisar de response em outro lugar no código, poderá transmitir a promessa. retornados diretamente por fetch, 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ê também não precisar do resultado fetch em outro lugar, poderá 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.

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

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

Divirta-se com o WebAssembly!