כשעובדים עם WebAssembly, בדרך כלל רוצים להוריד מודול, לקמפל אותו, ליצור מופע שלו ואז להשתמש בכל מה שהוא מייצא ב-JavaScript. בפוסט הזה מוסברת הגישה המומלצת שלנו ליעילות אופטימלית.
כשעובדים עם WebAssembly, בדרך כלל רוצים להוריד מודול, לקמפל אותו, ליצור מופע שלו ואז להשתמש בכל מה שהוא מייצא ב-JavaScript. הפוסט הזה מתחיל בקטע קוד נפוץ, אבל לא אופטימלי, שמבצע בדיוק את הפעולה הזו. לאחר מכן נדון בכמה אופטימיזציות אפשריות, ובסופו של דבר נציג את הדרך הפשוטה והיעילה ביותר להריץ WebAssembly מ-JavaScript.
קטע הקוד הזה מבצע את ריקוד הריקוד המלא של הורדה הידור, אבל באופן לא אופטימלי:
אל תשתמשו בזה!
(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);
})();
שימו לב איך אנחנו משתמשים ב-new WebAssembly.Module(buffer)
כדי להפוך מאגר תשובות למודול. זהו API סינכרוני, כלומר הוא חוסם את ה-thread הראשי עד שהוא מסתיים. כדי למנוע שימוש בו, Chrome משבית את WebAssembly.Module
למאגרים גדולים מ-4KB. כדי לעקוף את מגבלת הגודל, אפשר להשתמש במקום זאת ב-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)
עדיין לא הגישה האופטימלית, אבל נגיע לזה עוד רגע.
כמעט כל פעולה בקטע הקוד שעבר שינוי היא עכשיו אסינכרונית, כי השימוש ב-await
ברור. יוצא הדופן היחיד הוא new WebAssembly.Instance(module)
, עם אותה הגבלה של 4KB בגודל מאגר הנתונים הזמני ב-Chrome. כדי לשמור על עקביות ולאפשר לשרשור הראשי לפעול, אפשר להשתמש ב-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);
})();
נחזור לאופטימיזציה של compile
שציינתי קודם. באמצעות הידור באמצעות סטרימינג, הדפדפן כבר יכול להתחיל להדר את מודול WebAssembly בזמן ההורדה של הבייטים של המודול. מכיוון שההורדה וההדרכה מתבצעות במקביל, התהליך מהיר יותר – במיוחד כשמדובר בעומסי עבודה גדולים.
כדי להפעיל את האופטימיזציה הזו, צריך להשתמש ב-WebAssembly.compileStreaming
במקום ב-WebAssembly.compile
.
השינוי הזה מאפשר לנו גם להיפטר ממאגר המערך הביניים, כי עכשיו אנחנו יכולים להעביר ישירות את המופע של Response
שהוחזר על ידי 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);
})();
ה-API של WebAssembly.compileStreaming
מקבל גם הבטחה שמתקבלת ממנה מכונה של Response
. אם אין צורך ב-response
במקום אחר בקוד, אפשר להעביר את ההבטחה שמוחזרת על ידי fetch
ישירות, בלי לבצע 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);
})();
אם אתם לא צריכים את התוצאה fetch
גם במקום אחר, תוכלו אפילו להעביר אותה ישירות:
(async () => {
const module = await WebAssembly.compileStreaming(
fetch('fibonacci.wasm'));
const instance = await WebAssembly.instantiate(module);
const result = instance.exports.fibonacci(42);
console.log(result);
})();
אבל לדעתי, קל יותר לקרוא אותו כדי לשמור אותו בשורה נפרדת.
רואים איך אנחנו אוספים את התשובה למודול, ואז יוצרים מופע שלה באופן מיידי? מסתבר ש-WebAssembly.instantiate
יכול לבצע הידור ויצירת מופע בבת אחת. ממשק ה-API של WebAssembly.instantiateStreaming
עושה זאת בסטרימינג:
(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);
})();
אם אתם צריכים רק מכונה אחת, אין טעם לשמור את האובייקט module
, כך שתוכלו לפשט את הקוד עוד יותר:
// 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);
})();
אפשר לסכם את האופטימיזציות שביצענו באופן הבא:
- משתמשים בממשקי API אסינכרוניים כדי לא לחסום את ה-thread הראשי
- שימוש ב-API לסטרימינג כדי לקמפל מודולים של WebAssembly וליצור אותם במהירות רבה יותר
- לא כותבים קוד שאין בו צורך
נהנים עם WebAssembly!