WebAssembly-Module effizient laden

Wenn Sie mit WebAssembly arbeiten, möchten Sie häufig ein Modul herunterladen, kompilieren, instanziieren und dann alles, was es in JavaScript exportiert, verwenden. In diesem Beitrag wird unser empfohlener Ansatz für optimale Effizienz erläutert.

Wenn Sie mit WebAssembly arbeiten, möchten Sie häufig ein Modul herunterladen, kompilieren, instanziieren und dann alles, was es in JavaScript exportiert, verwenden. Dieser Beitrag beginnt mit einem allgemeinen, aber suboptimalen Code-Snippet, das genau das tut, erörtert mehrere mögliche Optimierungen und zeigt schließlich die einfachste und effizienteste Art, WebAssembly aus JavaScript auszuführen.

Dieses Code-Snippet führt den vollständigen Tanz aus, der heruntergeladen wird, kompiliert und instanziiert, allerdings auf suboptimale Weise:

Nicht verwenden!

(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, wie wir new WebAssembly.Module(buffer) verwenden, um einen Antwortpuffer in ein Modul umzuwandeln. Dies ist eine synchrone API, d. h., sie blockiert den Hauptthread, bis er abgeschlossen ist. Um eine solche Verwendung zu verhindern, deaktiviert Chrome WebAssembly.Module für Zwischenspeicher, die größer als 4 KB sind. Um die Größenbeschränkung zu umgehen, können Sie 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 das sehen wir uns gleich an.

Nahezu jeder Vorgang im geänderten Snippet ist jetzt asynchron, wie die Verwendung von await verdeutlicht. Die einzige Ausnahme ist new WebAssembly.Instance(module). Hier gilt in Chrome dieselbe Zwischenspeichergröße von 4 KB. Aus Konsistenzgründen und um den Hauptthread kostenlos zu halten, können Sie den asynchronen 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);
})();

Kehren wir zu der compile-Optimierung zurück, die ich bereits erwähnt habe. Mit der Streamingkompilierung kann der Browser bereits mit dem Kompilieren des WebAssembly-Moduls beginnen, während die Modulbyte noch heruntergeladen werden. Da Download und Kompilierung parallel erfolgen, geht dies schneller, insbesondere bei großen Nutzlasten.

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

Verwenden Sie WebAssembly.compileStreaming anstelle von WebAssembly.compile, um diese Optimierung zu aktivieren. Durch diese Änderung können wir auch den Arrayzwischenspeicher entsorgen, da wir jetzt die von await fetch(url) zurückgegebene Response-Instanz 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 eine Response-Instanz aufgelöst wird. Wenn du response an anderer Stelle im Code nicht benötigst, kannst du das von fetch zurückgegebene Promise direkt übergeben, ohne das Ergebnis explizit mit await zu versehen:

(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 sogar direkt übergeben:

(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 aber besser lesbar, wenn man eine separate Zeile verwendet.

Sehen wir uns an, wie wir die Antwort in ein Modul kompilieren und diese dann sofort instanziieren? Wie sich herausstellt, kann WebAssembly.instantiate mit einem einzigen Vorgang kompilieren und instanziieren. Die WebAssembly.instantiateStreaming API tut dies auf Streaming-Art:

(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, macht es keinen Sinn, das module-Objekt zu belassen, was den 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 angewendeten Optimierungen lassen sich wie folgt zusammenfassen:

  • Asynchrone APIs verwenden, damit der Hauptthread nicht blockiert wird
  • Streaming-APIs verwenden, um WebAssembly-Module schneller zu kompilieren und zu instanziieren
  • Kein Code schreiben, den Sie nicht benötigen

Viel Spaß mit WebAssembly!