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