טעינה יעילה של מודולי 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 יכול לבצע הידור (compile) ויצירת מופע בפעם אחת, אבל ה-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!