WebAssembly-Module effizient laden

Bei der Arbeit mit WebAssembly möchten Sie häufig ein Modul herunterladen, kompilieren, instanziieren und dann das exportierte JavaScript verwenden. In diesem Beitrag wird unser empfohlener Ansatz für eine optimale Effizienz erläutert.

Wenn Sie mit WebAssembly arbeiten, möchten Sie häufig ein Modul herunterladen, kompilieren, instanziieren und dann das exportierte JavaScript verwenden. Dieser Beitrag beginnt mit einem gängigen, aber suboptimalen Code-Snippet, das genau das tut. Es werden mehrere mögliche Optimierungen besprochen und schließlich die einfachste und effizienteste Methode zum Ausführen von WebAssembly aus JavaScript gezeigt.

Dieses Code-Snippet führt den gesamten Download-, Compile- und Instanziierungsvorgang aus, allerdings auf suboptimale Weise:

Verwenden Sie diese Option nicht.

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

Beachten Sie, dass wir mit new WebAssembly.Module(buffer) einen Antwortbuffer in ein Modul umwandeln. Dies ist eine synchrone API, d. h., der Hauptthread wird blockiert, bis die Ausführung abgeschlossen ist. Um die Verwendung zu erschweren, deaktiviert Chrome WebAssembly.Module für Puffer, die größer als 4 KB sind. Um das Größenlimit zu umgehen, können wir stattdessen await WebAssembly.compile(buffer) verwenden:

(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) ist immer noch nicht der optimale Ansatz, aber dazu kommen wir gleich.

Fast jeder Vorgang im geänderten Snippet ist jetzt asynchron, wie die Verwendung von await zeigt. Die einzige Ausnahme ist new WebAssembly.Instance(module), für das in Chrome dieselbe Beschränkung der Puffergröße von 4 KB gilt. Aus Gründen der Konsistenz und um den Hauptthread freizuhalten, können wir die asynchrone WebAssembly.instantiate(module) verwenden.

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

Kommen wir zurück zur compile-Optimierung, auf die ich vorhin hingewiesen habe. Bei der Streaming-Kompilierung kann der Browser mit der Kompilierung des WebAssembly-Moduls beginnen, während die Modul-Bytes noch heruntergeladen werden. Da Download und Kompilierung parallel erfolgen, ist dies schneller – insbesondere bei großen Nutzlasten.

Wenn die Downloadzeit länger als die Kompilierungszeit des WebAssembly-Moduls ist, wird die Kompilierung durch WebAssembly.compileStreaming() fast unmittelbar nach dem Herunterladen der letzten Bytes abgeschlossen.

Verwenden Sie WebAssembly.compileStreaming anstelle von WebAssembly.compile, um diese Optimierung zu aktivieren. Durch diese Änderung können wir auch den Zwischenarray-Puffer entfernen, da wir die von await fetch(url) zurückgegebene Response-Instanz jetzt direkt übergeben können.

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

Die WebAssembly.compileStreaming API akzeptiert auch ein Promise, das in einer Response-Instanz aufgelöst wird. Wenn Sie response nicht an anderer Stelle in Ihrem Code benötigen, können Sie das von fetch zurückgegebene Versprechen direkt übergeben, ohne das Ergebnis explizit zu await:

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

Wenn du das fetch-Ergebnis auch nicht an anderer Stelle benötigst, kannst du es auch direkt weitergeben:

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

Ich persönlich finde es jedoch leichter lesbar, wenn es in einer separaten Zeile steht.

Sehen Sie, wie wir die Antwort in ein Modul kompilieren und dann sofort instanziieren? Wie sich herausstellt, kann WebAssembly.instantiate in einem Durchgang kompiliert und instanziiert werden. Die WebAssembly.instantiateStreaming API führt dies per Streaming aus:

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

Wenn Sie nur eine Instanz benötigen, ist es nicht sinnvoll, das module-Objekt beizubehalten. Dadurch wird der Code weiter vereinfacht:

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

Die von uns durchgeführten Optimierungen können so zusammengefasst werden:

  • Verwenden Sie asynchrone APIs, um den Hauptthread nicht zu blockieren
  • Streaming-APIs verwenden, um WebAssembly-Module schneller zu kompilieren und zu instanziieren
  • Schreiben Sie keinen Code, den Sie nicht benötigen

Viel Spaß mit WebAssembly!