Podczas pracy z WebAssembly często konieczne jest pobranie modułu, skompilowanie go i utworzenie jego wystąpienia, a następnie użycie wyeksportowanych przez niego elementów w języku JavaScript. W tym poście opisujemy zalecane podejście zapewniające optymalną wydajność.
Podczas pracy z WebAssembly często trzeba pobrać moduł, skompilować go, utworzyć jego instancję, a potem użyć tego, co eksportuje w JavaScript. W tym wpisie zaczynamy od zwykłego, ale nieoptymalnego fragmentu kodu, który wykonuje dokładnie to, o czym mowa. Następnie omawiamy kilka możliwych optymalizacji, a na końcu pokazujemy najprostszy i najskuteczniejszy sposób uruchamiania WebAssembly z JavaScriptu.
Ten fragment kodu wykonuje pełny taniec pobierania, kompilacji i instancjonowania, ale w nieoptymalny sposób:
Nie używaj tego!
(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 new WebAssembly.Module(buffer)
do przekształcania bufora odpowiedzi w moduł. Jest to interfejs API synchroniczny, co oznacza, że blokuje wątek główny do czasu zakończenia. Aby zniechęcić do korzystania z tego mechanizmu, Chrome wyłącza WebAssembly.Module
w przypadku buforów większych niż 4 KB. Aby obejść limit rozmiaru, możemy użyć funkcji 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);
})();
await WebAssembly.compile(buffer)
nadal nie jest optymalnym rozwiązaniem, ale o tym za chwilę.
Prawie wszystkie operacje w zmodyfikowanym fragmencie kodu są teraz asynchroniczne, co widać po zastosowaniu instrukcji await
. Jedynym wyjątkiem jest new WebAssembly.Instance(module)
, który ma w Chrome te same ograniczenia rozmiaru bufora (4 KB). Ze względu na spójność i aby zachować porządek w głównym wątku, możemy użyć asynchronicznego 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 wspomnianej wcześniej optymalizacji compile
. Dzięki kompilacji strumieniowej przeglądarka może już zacząć kompilować moduł WebAssembly, gdy jego bajty są jeszcze pobierane. Ponieważ pobieranie i kompilowanie odbywają się równolegle, jest to szybsze – zwłaszcza w przypadku dużych ładunków.
Aby włączyć tę optymalizację, użyj wartości WebAssembly.compileStreaming
zamiast WebAssembly.compile
.
Ta zmiana pozwala nam też pozbyć się pośredniego bufora tablic, ponieważ możemy teraz bezpośrednio przekazywać instancję Response
zwracaną przez funkcję 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 API WebAssembly.compileStreaming
akceptuje też obietnicę, która przekształca się w instancję Response
. Jeśli nie potrzebujesz tagu response
w innym miejscu w kodzie, możesz bezpośrednio przekazać obietnicę zwróconą przez funkcję fetch
, bez await
jawności w 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 nie potrzebujesz wyniku fetch
w żadnym innym miejscu, 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, że lepiej jest umieścić je w osobnym wierszu.
Czy widzisz, jak kompilujemy odpowiedź w module, a potem natychmiast ją instancjuje? Jak się okazuje,
WebAssembly.instantiate
może skompilować i utworzyć instancję w jednym kroku. WebAssembly.instantiateStreaming
API 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 1 wystąpienia, nie ma sensu utrzymywać obiektu module
, ponieważ kod będzie wtedy jeszcze prostszy:
// 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);
})();
Zoptymalizowaliśmy kampanię w ten sposób:
- Używanie interfejsów API asynchronicznych, aby uniknąć blokowania wątku głównego
- Korzystanie z interfejsów API strumieniowych do szybszego kompilowania i tworzenia instancji modułów WebAssembly
- Nie pisz kodu, którego nie potrzebujesz
Miłej zabawy z WebAssembly