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.
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 await
wyniku:
(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!