Charger efficacement les modules WebAssembly

Lorsque vous travaillez avec WebAssembly, vous avez souvent besoin de télécharger un module, de le compiler, de l'instancier, puis d'utiliser tout ce qu'il exporte en JavaScript. Cet article explique l'approche que nous recommandons pour une efficacité optimale.

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

Cet extrait de code effectue la danse complète 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 comment 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 se termine. 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 dans l'extrait modifié sont désormais asynchrones, car l'utilisation de await le rend plus clair. La seule exception est new WebAssembly.Instance(module), qui utilise la même restriction de taille de mémoire tampon de 4 Ko dans Chrome. Par souci de cohérence et pour maintenir le thread principal libre, nous pouvons utiliser la méthode 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 que j'avais indiquée précédemment. Avec la compilation en streaming, le navigateur peut déjà commencer à compiler le module WebAssembly pendant le téléchargement des octets du module. Comme le téléchargement et la compilation ont lieu en parallèle, cette opération est plus rapide, en particulier pour les charges utiles volumineuses.

Lorsque le temps de téléchargement est plus long que celui du module WebAssembly, WebAssembly.compileStreaming() se termine 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 d'response ailleurs dans votre code, vous pouvez transmettre directement la promesse renvoyée par fetch, sans await expliciter 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 qu'il est plus lisible de le garder sur une ligne distincte cependant.

Vous voyez comment compiler la réponse dans un module, puis 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 par flux:

(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 des modules WebAssembly plus rapidement
  • N'écrivez pas de code dont vous n'avez pas besoin

Amusez-vous avec WebAssembly !