Charger efficacement les modules WebAssembly

Lorsque vous travaillez avec WebAssembly, vous souhaitez souvent télécharger un module, le compiler, l'instancier, puis utiliser tout ce qu'il exporte en JavaScript. Cet article explique l'approche recommandée pour une efficacité optimale.

Lorsque vous travaillez avec WebAssembly, vous souhaitez souvent télécharger un module, le compiler, l'instancier, puis utiliser tout ce qu'il exporte en JavaScript. Cet article commence par un extrait de code courant, mais non optimal, qui fait exactement cela. Il discute de plusieurs optimisations possibles et montre finalement le moyen le plus simple et le plus efficace d'exécuter WebAssembly à partir de JavaScript.

Cet extrait de code effectue la danse de téléchargement, de compilation et d'instanciation complète, mais de manière non optimale:

Ne l'utilisez pas.

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

Notez que nous utilisons new WebAssembly.Module(buffer) pour transformer un tampon de réponse en module. Il s'agit d'une API synchrone, ce qui signifie qu'elle bloque le thread principal jusqu'à ce qu'il soit terminé. Pour décourager son utilisation, Chrome désactive WebAssembly.Module pour les tampons de plus de 4 ko. Pour contourner la limite de taille, nous pouvons utiliser await WebAssembly.compile(buffer) à la place :

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

L'approche await WebAssembly.compile(buffer) n'est toujours pas optimale, mais nous y reviendrons dans un instant.

Presque toutes les opérations de l'extrait modifié sont désormais asynchrones, comme l'utilisation de await le montre clairement. La seule exception est new WebAssembly.Instance(module), qui a la même limite de taille de tampon de 4 Ko dans Chrome. Pour des raisons de cohérence et pour maintenir le thread principal libre, nous pouvons utiliser WebAssembly.instantiate(module) asynchrone.

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

Revenons à l'optimisation compile évoquée précédemment. Avec la compilation en streaming, le navigateur peut déjà commencer à compiler le module WebAssembly pendant que les octets du module sont encore en cours de téléchargement. Étant donné que le téléchargement et la compilation se font en parallèle, le processus est plus rapide, en particulier pour les charges utiles volumineuses.

Lorsque le temps de téléchargement est plus long que le temps de compilation du module WebAssembly, WebAssembly.compileStreaming() termine la compilation presque immédiatement après le téléchargement des derniers octets.

Pour activer cette optimisation, utilisez WebAssembly.compileStreaming au lieu de WebAssembly.compile. Cette modification nous permet également de nous débarrasser du tampon de tableau intermédiaire, car nous pouvons désormais transmettre directement l'instance Response renvoyée par 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 accepte également une promesse qui se résout en instance Response. Si vous n'avez pas besoin de response ailleurs dans votre code, vous pouvez transmettre directement la promesse renvoyée par fetch, sans awaiter explicitement son résultat:

(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 vous n'avez pas non plus besoin du résultat fetch ailleurs, vous pouvez même le transmettre directement:

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

Personnellement, je trouve que c'est plus lisible de les laisser sur une ligne distincte.

Voyons comment compiler la réponse dans un module pour l'instancier immédiatement. Il s'avère que WebAssembly.instantiate peut compiler et instancier en une seule fois. L'API WebAssembly.instantiateStreaming procède à cette opération en 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);
})();

Si vous n'avez besoin que d'une seule instance, il n'est pas utile de conserver l'objet module, ce qui simplifie encore le code:

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

Les optimisations que nous avons appliquées peuvent se résumer comme suit:

  • Utiliser des API asynchrones pour éviter de bloquer le thread principal
  • Utiliser des API de streaming pour compiler et instancier plus rapidement des modules WebAssembly
  • Ne pas écrire de code dont vous n'avez pas besoin

Amusez-vous avec WebAssembly !