Caricare i moduli WebAssembly in modo efficiente

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.

Quando il tempo di download è
più lungo del tempo di compilazione del modulo WebAssembly, poi WebAssembly.compileStreaming()
termina la compilazione quasi immediatamente dopo il download degli ultimi byte.

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 awaitapplicare 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!