כשעובדים עם 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. כדי לשמור על עקביות ולצורך שמירה על ה-thread הראשי
בחינם, אנחנו יכולים להשתמש
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
שהזכרתי קודם. באמצעות סטרימינג
compilation, הדפדפן כבר יכול
להתחיל להדר את מודול WebAssembly בזמן ההורדה של הבייטים של המודול. מאז ההורדה
ואיסוף ההידור מתבצע במקביל, זה מהיר יותר – במיוחד עם מטענים ייעודיים (payloads) גדולים.
כדי להפעיל את האופטימיזציה הזו, יש להשתמש ב-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!