טעינה יעילה של מודולי WebAssembly

כשעובדים עם 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, ואז WebAssembly.compileStreaming()
מסיים את ההידור כמעט מיד לאחר הורדת הבייטים האחרונים.

כדי להפעיל את האופטימיזציה הזו, יש להשתמש ב-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!