Quando si lavora con WebAssembly, spesso si desidera scaricare un modulo, compilarlo, creare un'istanza e quindi utilizzare qualsiasi cosa esporta in JavaScript. Questo post illustra il nostro approccio consigliato per un'efficienza ottimale.
Quando si lavora con WebAssembly, spesso si desidera scaricare un modulo, compilarlo, creare un'istanza e e poi usare ciò che esporta in JavaScript. Questo post inizia con un codice comune ma non ottimale lo snippet che fa esattamente questo, illustra diverse possibili ottimizzazioni e infine mostra il modo più semplice ed efficiente di eseguire WebAssembly da JavaScript.
Questo snippet di codice esegue la danza completa del processo di compilazione e download, anche se in modo non ottimale:
Non utilizzarlo!
(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);
})();
Nota come utilizziamo new WebAssembly.Module(buffer)
per trasformare un buffer di risposta in un modulo. Si tratta di un
sincrona, il che significa che blocca il thread principale fino al completamento. Per scoraggiarne l'utilizzo, Chrome
disabilita WebAssembly.Module
per i buffer di dimensioni superiori a 4 kB. Per aggirare il limite di dimensioni, possiamo
usa invece 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)
ancora non è l'approccio ottimale, ma lo vedremo più in dettaglio
secondo.
Quasi tutte le operazioni nello snippet modificato sono ora asincrone, in quanto l'utilizzo di await
rende
chiaro. L'unica eccezione è new WebAssembly.Instance(module)
, che ha lo stesso buffer da 4 kB
limitazione delle dimensioni in Chrome. Per coerenza e per mantenere il thread principale
senza costi, possiamo utilizzare
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);
})();
Torniamo all'ottimizzazione compile
a cui ho accennato in precedenza. Con flussi di dati
una compilazione automatica, il browser può già
inizia a compilare il modulo WebAssembly mentre i byte del modulo sono ancora in fase di download. Dal download
e la compilazione avviene in parallelo, il che è più veloce, soprattutto per i payload di grandi dimensioni.
Per attivare questa ottimizzazione, utilizza WebAssembly.compileStreaming
anziché WebAssembly.compile
.
Questa modifica ci consente anche di eliminare il buffer intermedio dell'array, poiché ora possiamo passare
Istanza Response
restituita direttamente da 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);
})();
L'API WebAssembly.compileStreaming
accetta anche una promessa che si risolve in un Response
in esecuzione in un'istanza Compute Engine. Se il codice response
non ti serve in altre parti del codice, puoi mantenere la promessa
restituito direttamente da fetch
, senza await
applicare esplicitamente il risultato:
(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 non hai bisogno del risultato di fetch
altrove, puoi anche passarlo direttamente:
(async () => {
const module = await WebAssembly.compileStreaming(
fetch('fibonacci.wasm'));
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();
Personalmente, però, trovo più leggibile tenerlo su una riga separata.
Vediamo come compiliamo la risposta in un modulo e poi creiamo un'istanza immediatamente? A quanto pare,
WebAssembly.instantiate
può compilare e creare un'istanza in una volta sola. La
L'API WebAssembly.instantiateStreaming
esegue questa operazione in modalità flusso:
(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 hai bisogno di una sola istanza, non ha senso tenere a portata di mano l'oggetto module
,
semplificando ulteriormente il codice:
// 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);
})();
Le ottimizzazioni che abbiamo applicato possono essere riassunte come segue:
- Usa le API asincrone per evitare di bloccare il thread principale
- Usa le API in modalità flusso per compilare e creare istanze più rapidamente dei moduli WebAssembly
- Non scrivere codice che non ti serve
Divertiti con WebAssembly!