Carga módulos de WebAssembly de manera eficiente

Cuando trabajes con WebAssembly, a menudo querrás descargar un módulo, compilarlo, crear una instancia y, luego, usar lo que exporte en JavaScript. En esta publicación, se explica nuestro enfoque recomendado para lograr una eficiencia óptima.

Cuando trabajas con WebAssembly, a menudo querrás descargar un módulo, compilarlo, crear una instancia y y, luego, usar lo que exporte en JavaScript. Esta publicación comienza con un código común pero subóptimo que hace precisamente eso, analiza varias optimizaciones posibles y, finalmente, muestra el la manera más simple y eficiente de ejecutar WebAssembly desde JavaScript.

Este fragmento de código realiza el baile completo de descarga, compilación e instancia, aunque de una manera deficiente:

¡No la uses!

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

Observa cómo usamos new WebAssembly.Module(buffer) para convertir un búfer de respuesta en un módulo. Este es un síncrona, lo que significa que bloquea el subproceso principal hasta que se completa. Para desalentar su uso, Chrome Inhabilita WebAssembly.Module para búferes superiores a 4 KB. Para evitar el límite de tamaño, podemos usa await WebAssembly.compile(buffer) en su lugar:

(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) aún no es el enfoque óptimo, pero lo haremos en un por segundo.

Casi todas las operaciones del fragmento modificado ahora son asíncronas, ya que el uso de await hace que claro. La única excepción es new WebAssembly.Instance(module), que tiene el mismo búfer de 4 KB. restricción de tamaño en Chrome. Por coherencia y para mantener el subproceso principal gratis, podemos usar la API de 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);
})();

Volvamos a la optimización de compile que indiqué anteriormente. Con transmisión continua compilación, el navegador ya puede comenzará a compilar el módulo WebAssembly mientras se descargan los bytes del módulo. Desde la descarga y la compilación suceden en paralelo, esto es más rápido, en especial para cargas útiles grandes.

Cuando el tiempo de descarga es
mayor que el tiempo de compilación del módulo WebAssembly, luego WebAssembly.compileStreaming()
finaliza la compilación casi inmediatamente después de que se descargan los últimos bytes.

Para habilitar esta optimización, usa WebAssembly.compileStreaming en lugar de WebAssembly.compile. Este cambio también nos permite deshacernos del búfer de array intermedio, ya que ahora podemos pasar await fetch(url) muestra directamente una instancia de 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);
})();

La API de WebAssembly.compileStreaming también acepta una promesa que se resuelve en un Response. instancia. Si no necesitas response en ninguna otra parte de tu código, puedes pasar la promesa. que muestra fetch directamente, sin await de manera explícita su 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);
})();

Si no necesitas el resultado de fetch en ningún otro lugar, puedes pasarlo directamente:

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

Sin embargo, personalmente me parece más legible tenerlo en una línea separada.

¿Ves cómo compilamos la respuesta en un módulo y creamos una instancia de inmediato? Resulta que, WebAssembly.instantiate se puede compilar y crear una instancia de una sola vez. El La API de WebAssembly.instantiateStreaming lo hace de manera de transmisión:

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

Si solo necesitas una instancia, no tiene sentido mantener el objeto module. simplificando aún más el 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);
})();

Las optimizaciones que aplicamos se pueden resumir de la siguiente manera:

  • Usa APIs asíncronas para evitar bloquear el subproceso principal
  • Usa APIs de transmisión para compilar módulos de WebAssembly y crear instancias más rápido
  • No escribas código que no necesitas.

¡Diviértete con WebAssembly!