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. Ce post explique l'approche que nous recommandons pour une efficacité optimale.

Lorsque vous travaillez avec WebAssembly, vous avez souvent besoin de télécharger un module, de le compiler, de l'instancier et puis utiliser ce qu'elle exporte en JavaScript. Ce post commence par un code courant, mais non optimal. l'extrait ci-dessus, décrit plusieurs optimisations possibles, puis affiche l'élément 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'un 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 Utilisez plutôt 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);
})();

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

Presque toutes les opérations de l'extrait modifié sont désormais asynchrones, car l'utilisation de await fait . La seule exception est new WebAssembly.Instance(module), qui possède le même tampon de 4 Ko. la restriction de taille applicable dans Chrome. Par souci de cohérence et pour conserver le thread principal sans frais, nous pouvons utiliser la méthode 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);
})();

Revenons à l'optimisation compile évoquée précédemment. Avec le streaming compilation, le navigateur peut déjà commencer à compiler le module WebAssembly pendant le téléchargement des octets du module. Depuis le téléchargement et la compilation en parallèle, c'est plus rapide, surtout pour les charges utiles volumineuses.

Lorsque le temps de téléchargement est
que le temps de compilation du module WebAssembly, alors WebAssembly.compileStreaming()
se 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 intermédiaire du tableau, car nous pouvons désormais transmettre le Instance Response renvoyée directement 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 Response Compute Engine. Si vous n'avez pas besoin de response ailleurs dans votre code, vous pouvez transmettre la promesse renvoyé directement par fetch, sans ajouter explicitement await à 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 qu'il est plus lisible de le garder sur une ligne séparée, cependant.

Voyons comment compiler la réponse dans un module pour l'instancier immédiatement. Il se trouve que WebAssembly.instantiate peut être compilé et instancié en une seule fois. La 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 ne sert à rien de conserver l'objet module à portée de main. en simplifiant 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 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 rédigez pas de code dont vous n'avez pas besoin

Amusez-vous bien avec WebAssembly !