Efektywne wczytywanie modułów WebAssembly

Podczas pracy z WebAssembly często chcesz pobrać moduł, skompilować go, utworzyć jego instancję i użyć wszystkich wyeksportowanych danych w kodzie JavaScript. W tym poście omówiliśmy zalecane przez nas podejście do uzyskiwania optymalnej efektywności.

Podczas pracy z WebAssembly często chcesz pobrać moduł, skompilować go, utworzyć jego instancję, a następnie użyć wyeksportowanych danych w kodzie JavaScript. Ten post zaczyna się od popularnego, ale nieoptymalnego fragmentu kodu, który właśnie to robi. Omawiamy kilka możliwych optymalizacji i przedstawiamy najprostszy i najwydajniejszy sposób uruchamiania WebAssembly z użyciem JavaScriptu.

Ten fragment kodu wykonuje kompletny taniec w pobieraniu, kompilowany do pobierania, jednak w nieoptymalny sposób:

Nie używaj go.

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

Zwróć uwagę, jak używamy funkcji new WebAssembly.Module(buffer) do przekształcania bufora odpowiedzi w moduł. Jest to synchroniczny interfejs API, co oznacza, że blokuje on wątek główny do czasu zakończenia działania. Aby odmówić jego używania, Chrome wyłącza WebAssembly.Module w przypadku buforów większych niż 4 KB. Aby obejść limit rozmiaru, możemy zamiast tego użyć 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);
})();

Metoda await WebAssembly.compile(buffer) nadal nie jest najlepszym rozwiązaniem, ale wrócimy do tego za chwilę.

Prawie każda operacja w zmodyfikowanym fragmencie kodu jest teraz asynchroniczna, ponieważ użycie polecenia await jasno sygnalizuje. Jedynym wyjątkiem jest new WebAssembly.Instance(module), który ma takie samo ograniczenie rozmiaru bufora w Chrome 4 KB. Aby zachować spójność i utrzymać bezpłatny wątek główny, możemy użyć asynchronicznej 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);
})();

Wróćmy do optymalizacji compile, o której wspomnieliśmy wcześniej. Dzięki kompilacji strumieniowania przeglądarka może już zacząć kompilować moduł WebAssembly w trakcie pobierania jego bajtów. Pobieranie i kompilacja odbywa się równolegle, co przyspiesza ten proces, zwłaszcza w przypadku dużych ładunków.

Jeśli czas pobierania jest dłuższy niż czas kompilacji modułu WebAssembly, funkcja WebAssembly.kompilowa-Streaming() kończy kompilację niemal natychmiast po pobraniu ostatnich bajtów.

Aby włączyć tę optymalizację, użyj WebAssembly.compileStreaming zamiast WebAssembly.compile. Ta zmiana pozwoli nam też pozbyć się bufora pamięci pośredniej, ponieważ teraz możemy bezpośrednio przekazywać instancję Response zwracaną przez 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);
})();

Interfejs WebAssembly.compileStreaming API akceptuje też obietnicę, która zwraca się do instancji Response. Jeśli nie potrzebujesz parametru response w innym miejscu w kodzie, możesz zrealizować obietnicę zwrotną przez usługę fetch bezpośrednio, nie podając jej konkretnego awaitwyniku:

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

Jeśli w innym miejscu wynik fetch nie jest potrzebny, możesz go przekazać bezpośrednio:

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

Osobiście uważam jednak, że lepiej jest zapisać go w osobnym wierszu.

Zobacz, jak skompilować odpowiedź w moduł, a następnie od razu utworzyć jej instancję. Okazuje się, że WebAssembly.instantiate może kompilować i tworzyć wystąpienia za jednym razem, a interfejs API WebAssembly.instantiateStreaming robi to w sposób strumieniowy:

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

Jeśli potrzebujesz tylko jednej instancji, nie ma sensu utrzymywać obiektu module w pobliżu, aby jeszcze bardziej uprościć kod:

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

Podsumowanie zastosowanych optymalizacji:

  • Użyj asynchronicznych interfejsów API, aby uniknąć blokowania wątku głównego
  • Używaj interfejsów API strumieniowania do szybszego kompilowania i tworzenia instancji modułów WebAssembly
  • Nie pisz kodu, którego nie potrzebujesz

Baw się z WebAssembly!