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. Ce post explique l'approche que nous recommandons 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 l'ensemble des opérations de téléchargement, de compilation et d'instanciation, 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);
})();

await WebAssembly.compile(buffer) n'est toujours pas l'approche 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 présente la même restriction 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 de compile dont j'ai parlé 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 ont lieu en parallèle, cela 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 la promesse renvoyée par fetch directement, sans await 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 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 être compilé et instancié en une seule fois. L'API WebAssembly.instantiateStreaming effectue cette opération en flux continu:

(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 est inutile de conserver l'objet module, ce qui simplifie davantage 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 être résumées 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 rédigez pas de code dont vous n'avez pas besoin

Amusez-vous bien avec WebAssembly !