Quando lavori con WebAssembly, spesso vuoi scaricare un modulo, compilarlo, eseguirlo e poi utilizzare tutto ciò che esporta in JavaScript. Questo post illustra il nostro approccio consigliato per un'efficienza ottimale.
Quando lavori con WebAssembly, spesso vuoi scaricare un modulo, compilarlo, eseguirlo e poi utilizzare tutto ciò che esporta in JavaScript. Questo post inizia con uno snippet di codice comune, ma non ottimale, che fa esattamente questo, discute diverse possibili ottimizzazioni e infine mostra il modo più semplice ed efficiente per eseguire WebAssembly da JavaScript.
Questo snippet di codice esegue l'intera procedura di download, compilazione e istanza, anche se in modo non ottimale:
Non utilizzare questa opzione.
(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);
})();
Tieni presente come utilizziamo new WebAssembly.Module(buffer)
per trasformare un buffer di risposta in un modulo. Si tratta di un'API
sincrona, il che significa che blocca il thread principale fino al completamento. Per scoraggiarne l'utilizzo, Chrome disattiva WebAssembly.Module
per i buffer più grandi di 4 KB. Per aggirare il limite di dimensioni, possiamo utilizzare 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)
non è ancora l'approccio ottimale, ma lo vedremo tra un secondo.
Quasi tutte le operazioni nello snippet modificato sono ora asincrone, come è evidente dall'utilizzo di await
. L'unica eccezione è new WebAssembly.Instance(module)
, che ha la stessa limitazione di dimensione del buffer di 4 KB in Chrome. Per coerenza e per mantenere libero il thread principale, possiamo utilizzare il metodo asincronoWebAssembly.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 di compile
a cui ho accennato prima. Con la compilazione dinamica, il browser può già iniziare a compilare il modulo WebAssembly mentre i byte del modulo sono ancora in fase di download. Poiché il download e la compilazione vengono eseguiti in parallelo, questa operazione è più rapida, in particolare per i payload di grandi dimensioni.
Per attivare questa ottimizzazione, utilizza WebAssembly.compileStreaming
anziché WebAssembly.compile
.
Questa modifica ci consente inoltre di eliminare l'array buffer intermedio, poiché ora possiamo passare direttamente l'istanza Response
restituita 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'istanza Response
. Se non hai bisogno di response
altrove nel codice, puoi passare direttamente la promessa
rimessa da fetch
, senza await
arne 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 fetch
anche 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 lasciarlo in una riga separata.
Hai visto come compiliamo la risposta in un modulo e poi la istanziamo immediatamente? A quanto pare,
WebAssembly.instantiate
può compilare e creare istanze contemporaneamente. L'API
WebAssembly.instantiateStreaming
esegue questa operazione in 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 hai bisogno di una sola istanza, non ha senso mantenere 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:
- Utilizza API asincrone per evitare di bloccare il thread principale
- Utilizza le API di streaming per compilare e creare istanze dei moduli WebAssembly più rapidamente
- Non scrivere codice non necessario
Buon divertimento con WebAssembly.