Cuando trabajas con WebAssembly, a menudo quieres descargar un módulo, compilarlo, crear una instancia y, luego, usar lo que exporta en JavaScript. En esta publicación, se explica nuestro enfoque recomendado para lograr una eficiencia óptima.
Cuando trabajas con WebAssembly, a menudo quieres descargar un módulo, compilarlo, crear una instancia y, luego, usar lo que exporta en JavaScript. Esta publicación comienza con un fragmento de código común, pero poco óptimo, que hace exactamente eso, analiza varias optimizaciones posibles y, finalmente, muestra la forma más simple y eficiente de ejecutar WebAssembly desde JavaScript.
Este fragmento de código realiza el baile completo de descarga, compilación y creación de instancias, aunque de forma no óptima:
¡No lo 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. Esta es una API síncrona, lo que significa que bloquea el subproceso principal hasta que se completa. Para desalentar su uso, Chrome inhabilita WebAssembly.Module
para los búferes de más de 4 KB. Para evitar el límite de tamaño, podemos usar 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 hablaremos de eso en un segundo.
Casi todas las operaciones del fragmento modificado ahora son asíncronas, como lo deja claro el uso de await
. La única excepción es new WebAssembly.Instance(module)
, que tiene la misma restricción de tamaño de búfer de 4 KB en Chrome. Para mantener la coherencia y mantener el subproceso principal libre, podemos usar el WebAssembly.instantiate(module)
así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);
})();
Volvamos a la optimización de compile
a la que hice referencia antes. Con la compilación en tiempo real, el navegador ya puede comenzar a compilar el módulo de WebAssembly mientras los bytes del módulo aún se descargan. Como la descarga y la compilación se realizan en paralelo, esto es más rápido, en especial para cargas útiles grandes.
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 directamente la instancia de Response
que muestra 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);
})();
La API de WebAssembly.compileStreaming
también acepta una promesa que se resuelve en una instancia de Response
. Si no necesitas response
en otra parte de tu código, puedes pasar la promesa que muestra fetch
directamente, sin await
su resultado 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);
})();
Si tampoco necesitas el resultado de fetch
en otro lugar, incluso 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, creo que es más legible mantenerlo en una línea separada.
¿Viste cómo compilamos la respuesta en un módulo y, luego, creamos una instancia de él de inmediato? Resulta que WebAssembly.instantiate
puede compilar y crear instancias de una sola vez. La API de WebAssembly.instantiateStreaming
lo hace de forma continua:
(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
, lo que simplifica 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 y crear instancias de módulos de WebAssembly más rápido
- No escribas código que no necesites
¡Diviértete con WebAssembly!