החלפת נתיב חם ב-JavaScript של האפליקציה שלך באמצעות WebAssembly

המהירות שלו עקבית

במאמרים הקודמים שלי, תיארתי איך WebAssembly מאפשר להביא את סביבת הספריות של C/C++ לאינטרנט. אפליקציה אחת שמשתמשת באופן נרחב בספריות C/C++ היא squoosh, אפליקציית האינטרנט שלנו שמאפשרת לדחוס תמונות באמצעות מגוון קודקים שעבר הידור מ-C++ ל-WebAssembly.

WebAssembly היא מכונה וירטואלית ברמה נמוכה שמריצה את קוד הבייט ששמור בקובצי .wasm. קוד הבייט הזה מוגדר בתבנית חזקה ומבנה בצורה כזו שאפשר לקמפל אותו ולבצע אופטימיזציה עבור מערכת המארח מהר יותר מאשר ב-JavaScript. WebAssembly מספקת סביבה להרצת קוד שהתבססה על הרצה בארגז חול (sandboxing) והטמעה כבר מההתחלה.

מניסיוני, רוב הבעיות בביצועים באינטרנט נובעות מפריסה מאולצת ומבדיקת צבע מוגזמת, אבל מדי פעם האפליקציה צריכה לבצע משימה יקרה מבחינת המחשוב שנמשכת הרבה זמן. כאן יכול לעזור WebAssembly.

הנתיב החם

ב-squoosh, כתבנו פונקציית JavaScript שמסובבת מאגר נתונים זמני של תמונות בכפולות של 90 מעלות. אמנם OffscreenCanvas מושלם למטרה הזו, אבל הוא לא נתמך בכל הדפדפנים שאליהם טירגטנו, ויש בו באגים מסוימים ב-Chrome.

הפונקציה הזו עוברת על כל פיקסל בתמונה הקלט ומעתיקה אותו למיקום אחר בתמונה הפלט כדי לבצע סיבוב. בתמונה בגודל 4,094 על 4,096 פיקסלים (16 מגה פיקסלים), נדרשות יותר מ-16 מיליון חזרות על בלוק הקוד הפנימי, שנקרא 'נתיב חם'. למרות מספר החזרות הגדול, שניים מתוך שלושת הדפדפנים שבדקנו סיימו את המשימה תוך 2 שניות או פחות. משך זמן מקובל לאינטראקציה מהסוג הזה.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

עם זאת, בדפדפן אחד הזמן הוא יותר מ-8 שניות. הדרך שבה דפדפנים מבצעים אופטימיזציה של JavaScript היא מאוד מורכבת, ומנועים שונים מבצעים אופטימיזציה לדברים שונים. חלק מהן מבצעות אופטימיזציה להרצה גולמית, וחלקן מבצעות אופטימיזציה לאינטראקציה עם DOM. במקרה הזה, נתקלת בנתיב שלא עבר אופטימיזציה בדפדפן אחד.

לעומת זאת, WebAssembly מבוסס כולו על מהירות ביצוע גולמית. לכן, אם רוצים ביצועים מהירים וחזויים בדפדפנים שונים לקוד כזה, אפשר להיעזר ב-WebAssembly.

WebAssembly לביצועים צפויים

באופן כללי, JavaScript ו-WebAssembly יכולים להשיג את אותה רמת ביצועים שיא. עם זאת, ב-JavaScript אפשר להגיע לביצועים האלה רק ב'נתיב המהיר', ולעיתים קרובות קשה להישאר ב'נתיב המהיר' הזה. אחד היתרונות המרכזיים של WebAssembly הוא הביצועים הצפויים, גם בדפדפנים שונים. הסיווג המחמיר והארכיטקטורה ברמה נמוכה מאפשרים למהדר לספק ערבויות חזקות יותר, כך שקוד WebAssembly צריך לעבור אופטימיזציה רק פעם אחת ותמיד ישתמש ב'נתיב המהיר'.

כתיבה ל-WebAssembly

בעבר לקחנו ספריות C/C++‎ וקמפילנו אותן ל-WebAssembly כדי להשתמש בפונקציונליות שלהן באינטרנט. לא נגענו בקוד של הספריות, רק כתבנו כמויות קטנות של קוד C/C++‎ כדי ליצור את הגשר בין הדפדפן לספרייה. הפעם המוטיבציה שלנו שונה: אנחנו רוצים לכתוב משהו מאפס תוך התמקדות ב-WebAssembly, כדי שנוכל לנצל את היתרונות של WebAssembly.

הארכיטקטורה של WebAssembly

כשכותבים ל-WebAssembly, כדאי להבין קצת יותר מהו WebAssembly בפועל.

ציטוט מ-WebAssembly.org:

כשמפעילים הידור של קטע קוד ב-C או ב-Rust ל-WebAssembly, מתקבל קובץ .wasm שמכיל הצהרת מודול. ההצהרה הזו מורכבת מרשימת 'ייבוא' שהמודול מצפה לסביבה שלו, מרשימת ייצוא שהמודול הזה מאפשר למארח (פונקציות, קבועים, קטעי זיכרון) וכמובן מההוראות הבינאריות בפועל של הפונקציות שמפורטות בה.

משהו שלא הבנתי עד שבדקתי את זה: המקבץ שהופך את WebAssembly ל'מכונה וירטואלית מבוססת סטאק' לא מאוחסנת בקטע הזיכרון שבו משתמשים מודולים של WebAssembly. הסטאק נמצא בתוך המכונה הווירטואלית לחלוטין, ואין למפתחי האתרים גישה אליו (חוץ דרך כלי הפיתוח). לכן אפשר לכתוב מודולים של WebAssembly שלא זקוקים בכלל לזיכרון נוסף ומשתמשים רק בסטאק הפנימי של המכונה הווירטואלית.

במקרה שלנו, נצטרך להשתמש בזיכרון נוסף כדי לאפשר גישה שרירותית לפיקסלים של התמונה וליצור גרסת סיבוב של התמונה הזו. לשם כך משמש השדה WebAssembly.Memory.

ניהול זיכרון

בדרך כלל, אחרי שמשתמשים בנפח זיכרון נוסף, צריך לנהל אותו בצורה כלשהי. אילו חלקים מהזיכרון נמצאים בשימוש? אילו בחינם? לדוגמה, ב-C יש את הפונקציה malloc(n) שמוצאת מרחב זיכרון של n בייטים רצופים. פונקציות מהסוג הזה נקראות גם "מקצים". כמובן, ההטמעה של מנהל הקצאת הזיכרון שבשימוש צריכה להיכלל במודול WebAssembly, והיא תגדיל את גודל הקובץ. הגודל והביצועים של פונקציות ניהול הזיכרון האלה עשויים להשתנות באופן משמעותי בהתאם לאלגוריתם שבו נעשה שימוש. לכן, בשפות רבות יש כמה הטמעות לבחירה (dmalloc,‏ emmalloc,‏ wee_alloc וכו').

במקרה שלנו, אנחנו יודעים את המימדים של קובץ הקלט (ולכן את המימדים של קובץ הפלט) לפני שאנחנו מריצים את מודול WebAssembly. כאן גילינו הזדמנות: בדרך כלל, נעביר את מאגר הנתונים הזמני של תמונת הקלט כפרמטר לפונקציית WebAssembly ונחזיר את התמונה המסובבת כערך החזרה. כדי ליצור את הערך המוחזר, נצטרך להשתמש במקצה. אבל מכיוון שאנחנו יודעים את נפח הזיכרון הכולל הנדרש (פי שניים מגודל התמונה להזנה, פעם להזנה ופעם לפלט), אנחנו יכולים להעביר את התמונה להזנה לזיכרון של WebAssembly באמצעות JavaScript, להריץ את מודול WebAssembly כדי ליצור תמונה שנייה, מסובבת, ואז להשתמש ב-JavaScript כדי לקרוא חזרה את התוצאה. אנחנו יכולים להסתדר בלי להשתמש בכלל בניהול זיכרון.

יש מבחר גדול

אם תבחנו את פונקציית ה-JavaScript המקורית שאותה אנחנו רוצים להפעיל ב-WebAssembly-fy, תוכלו לראות שמדובר בקוד חישובי בלבד ללא ממשקי API ספציפיים ל-JavaScript. לכן, קל יחסית להעביר את הקוד הזה לכל שפה. בדקנו 3 שפות שונות שאפשר להדרש ל-WebAssembly: C/C++, ‏ Rust ו-AssemblyScript. השאלה היחידה שצריך לענות עליה בכל אחת מהשפות היא: איך ניגשים לזיכרון גולמי בלי להשתמש בפונקציות לניהול זיכרון?

C ו-Emscripten

Emscripten הוא מהדר C ליעד WebAssembly. המטרה של Emscripten היא לשמש כתחליף ללא תהליך העברה למהדרי C ידועים כמו GCC או clang, והוא תואם ברוב הדגלים. זהו חלק מרכזי במשימה של Emscripten, כי היא רוצה להפוך את הידור של קוד C ו-C++ קיים ל-WebAssembly בקלות רבה ככל האפשר.

גישה לזיכרון גולמי היא מעין קוד C, והסמנים קיימים בדיוק מהסיבה הזו:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

כאן אנחנו הופכים את המספר 0x124 למצב מצביע למספרים שלמים (או בייטים) לא חתומים של 8 ביט. הפעולה הזו הופכת את המשתנה ptr למערך שמתחיל בכתובת הזיכרון 0x124, שאפשר להשתמש בו כמו בכל מערך אחר, ומאפשרת לנו לגשת ביטים ספציפיים לצורך קריאה וכתיבה. במקרה שלנו, אנחנו מדברים על מאגר RGBA של תמונה שרוצים לשנות את הסדר שלה כדי לבצע סיבוב. כדי להעביר פיקסל, צריך להעביר 4 בייטים רצופים בבת אחת (בייט אחד לכל ערוץ: R,‏ G,‏ B ו-A). כדי לעשות זאת בקלות, אפשר ליצור מערך של מספרים שלמים 32-ביט ללא סימן. לפי הסכמה, תמונה הקלט תתחיל בכתובת 4 ותמונת הפלט תתחיל מיד אחרי שתמונת הקלט תסתיים:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

אחרי העברת כל פונקציית JavaScript ל-C, אפשר לקמפל את קובץ ה-C באמצעות emcc:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

כמו תמיד, emscripten יוצר קובץ של קוד דבק שנקרא c.js ומודול wasm שנקרא c.wasm. הערה: קובץ ה-gzip של מודול ה-wasm הוא רק כ-260 בייטים, בעוד שקוד הדבקה הוא כ-3.5KB אחרי gzip. אחרי כמה ניסיונות, הצלחנו להיפטר מקוד הדבקה וליצור מופע של מודולי WebAssembly באמצעות ממשקי ה-API הרגילים. בדרך כלל אפשר לעשות זאת ב-Emscripten, כל עוד לא משתמשים בתוכן מהספרייה הרגילה של C.

Rust

Rust היא שפת תכנות חדשה ומודרנית עם מערכת סוגים עשירה, ללא סביבת זמן ריצה ומודל בעלות שמבטיח בטיחות בזיכרון ובחוט. ב-Rust יש גם תמיכה ב-WebAssembly כתכונה מרכזית, וצוות Rust תרם הרבה כלים מצוינים לסביבת WebAssembly.

אחד מהכלים האלה הוא wasm-pack, על ידי קבוצת העבודה חלודה. wasm-pack לוקח את הקוד והופך אותו למודול ידידותי לאינטרנט שפועל באופן ייחודי עם Bundlers כמו webpack. wasm-pack הוא כלי נוח מאוד, אבל כרגע הוא פועל רק ב-Rust. הקבוצה שוקלת להוסיף תמיכה בשפות אחרות שמטרגטות WebAssembly.

ב-Rust, פלחים הם מה שמשתמשים בו במקום מערכי מחרוזות ב-C. וגם כמו ב-C, אנחנו צריכים ליצור פלחים שמשתמשים בכתובות ההתחלה שלנו. הפעולה הזו מנוגדת למודל הבטיחות של הזיכרון ש-Rust אוכפת, ולכן כדי להגיע למטרה שלנו אנחנו צריכים להשתמש במילות המפתח unsafe, שמאפשרות לנו לכתוב קוד שלא עומד בדרישות של המודל הזה.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

הידור קובצי Rust באמצעות

$ wasm-pack build

מניב מודול Wam בנפח 7.6KB עם כ-100 בייטים של קוד דבק (שניהם אחרי gzip).

AssemblyScript

AssemblyScript הוא פרויקט די צעיר שמטרתו להיות מהדר מסוג TypeScript-to-WebAssembly. עם זאת, חשוב לציין שהוא לא צורך רק TypeScript. ב-AssemblyScript נעשה שימוש באותו תחביר כמו ב-TypeScript, אבל הספרייה הרגילה מוחלפת בספרייה משלהם. הספרייה הרגילה שלהם משמשת לבניית מודלים של היכולות של WebAssembly. המשמעות היא שאי אפשר פשוט לקמפל כל קובץ TypeScript שנמצא ברשותכם ל-WebAssembly, אבל כן המשמעות היא שאתם לא צריכים ללמוד שפת תכנות חדשה כדי לכתוב ב-WebAssembly.

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

בגלל סוג הפלטפורמה הקטן של rotate(), היה אפשר לנייד את הקוד הזה ל-AssemblyScript בקלות. הפונקציות load<T>(ptr: usize) ו-store<T>(ptr: usize, value: T) הן פונקציות של AssemblyScript שמאפשרות לגשת לזיכרון גולמי. כדי להדר את קובץ AssemblyScript, צריך רק להתקין את חבילת ה-npm AssemblyScript/assemblyscript ולהריץ את הפקודה

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript יספק לנו מודול wasm של כ-300 בייטים וללא קוד דבק. המודול פועל רק עם ממשקי ה-API הרגילים של WebAssembly.

WebAssembly Forensics

קובץ ה-Rust בגודל 7.6KB גדול באופן מפתיע בהשוואה לשתי השפות האחרות. יש כמה כלים בסביבת WebAssembly שיכולים לעזור לכם לנתח את קובצי WebAssembly (ללא קשר לשפה שבה הם נוצרו), לומר לכם מה קורה ולעזור לכם לשפר את המצב.

Twiggy

Twiggy הוא כלי נוסף מצוות WebAssembly של Rust, שמאפשר לחלץ מידע מועיל ממודול WebAssembly. הכלי לא ספציפי ל-Rust ומאפשר לבדוק דברים כמו תרשים הקריאות של המודול, לקבוע אילו קטעים לא בשימוש או מיותרים ולגלות אילו קטעים תורמים לגודל הקובץ הכולל של המודול. אפשר לבצע את הפעולה השנייה באמצעות הפקודה top של Twiggy:

$ twiggy top rotate_bg.wasm
צילום מסך של התקנת Twiggy

במקרה הזה, ניתן לראות שרוב גודל הקובץ נובע מהמקצה. זה היה מפתיע כי בקוד שלנו לא נעשה שימוש בהקצאות דינמיות. גורם חשוב נוסף הוא תת-קטע בשם 'שמות של פונקציות'.

wasm-strip

wasm-strip הוא כלי מ-WebAssembly Binary Toolkit, או wabt בקיצור. הוא מכיל כמה כלים שמאפשרים לבדוק ולבצע פעולות על מודולים של WebAssembly. wasm2wat הוא מנתח קוד שממיר מודול wasm בינארי לפורמט קריא לבני אדם. Wabt מכיל גם את wat2wasm, שמאפשר להפוך את הפורמט הקריא לאנשים חזרה למודול בינארי של wasm. אמנם השתמשנו בשני הכלים המשלימים האלה כדי לבדוק את קובצי ה-WebAssembly שלנו, אבל נראה ש-wasm-strip הוא השימושי ביותר. הפקודה wasm-strip מסירה קטעים ומטא-נתונים מיותרים ממודול WebAssembly:

$ wasm-strip rotate_bg.wasm

כך גודל הקובץ של מודול החלודה יקטן מ-7.5KB ל-6.6KB (לאחר gzip).

wasm-opt

wasm-opt הוא כלי מ-Binaryen. הכלי מקבל מודול WebAssembly ומנסה לבצע אופטימיזציה שלו גם לגבי גודל וגם לגבי ביצועים, על סמך קוד הבייט בלבד. כלים מסוימים, כמו Emscripten, כבר מפעילים את הכלי הזה, וחלקם לא. בדרך כלל מומלץ לנסות לחסוך עוד כמה בייטים באמצעות הכלים האלה.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

בעזרת wasm-opt אפשר לחסוך עוד כמה בייטים, כך שיישארו בסך 6.2KB אחרי gzip.

#![no_std]

אחרי התייעצות ומחקר, שכתבנו את קוד חלודה שלנו בלי להשתמש בספרייה הרגילה של Rust, באמצעות התכונה #![no_std]. הפעולה הזו משביתה גם את הקצאות הזיכרון הדינמיות לגמרי, ומסירה את הקוד של מנהל ההקצאות מהמודול שלנו. הידור קובץ Rust הזה באמצעות

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

הפיק מודול Wasm בגודל 1.6KB אחרי wasm-opt, ‏ wasm-strip ו-gzip. הוא עדיין גדול יותר מהמודולים שנוצרו על ידי C ו-AssemblyScript, אבל הוא קטן מספיק כדי להיחשב כמכשיר קל.

ביצועים

לפני שאנחנו מגיעים למסקנות על סמך גודל הקובץ בלבד, חשוב לדעת שהמטרה של השינוי היא לשפר את הביצועים, ולא את גודל הקובץ. אז איך מדדו את הביצועים ומה היו התוצאות?

איך מבצעים השוואה לשוק

למרות ש-WebAssembly הוא פורמט בייטקוד ברמה נמוכה, עדיין צריך לשלוח אותו דרך מהדר כדי ליצור קוד מכונה ספציפי למארח. בדומה ל-JavaScript, גם המהדר פועל במספר שלבים. במילים פשוטות: השלב הראשון מהיר יותר בתהליך הידור הקוד, אבל הוא נוטה ליצור קוד איטי יותר. אחרי שהמודול מתחיל לפעול, הדפדפן בודק אילו חלקים נמצאים בשימוש תדיר ושולח אותם דרך מהדר שמבצע אופטימיזציה טובה יותר אבל איטי יותר.

התרחיש לדוגמה שלנו מעניין מכיוון שהקוד לסיבוב תמונה ישמש פעם אחת, אולי פעמיים. כך שברוב המכריע של המקרים אף פעם לא ייהנו מהיתרונות של המהדר (compiler) לאופטימיזציה. חשוב לזכור את זה כשמבצעים השוואה למדד ביצועים. אם תריצו מודולים של WebAssembly 10,000 פעמים בלולאה, התוצאות לא יהיו מציאותיות. כדי לקבל מספרים ריאליסטיים, צריך להריץ את המודול פעם אחת ולקבל החלטות על סמך המספרים מההפעלה היחידה הזו.

השוואת ביצועים

השוואת מהירות לפי שפה
השוואת מהירות לפי דפדפן

שני הגרפים האלה הם תצוגות שונות של אותם נתונים. בתרשים הראשון אנחנו מבצעים השוואה לפי דפדפן, ובתרשים השני אנחנו מבצעים השוואה לפי שפה שבה נעשה שימוש. חשוב לציין שבחרתי בסולם זמן לוגריתמי. חשוב גם לציין שבכל הבדיקות השתמשו באותה תמונה לבדיקה של 16 מגה-פיקסלים ובאותה מכונת מארח, מלבד דפדפן אחד שלא ניתן היה להריץ באותה מכונה.

בלי לנתח יותר מדי את התרשימים האלה, ברור שפתרנו את בעיית הביצועים המקורית: כל המודולים של WebAssembly פועלים תוך כ-500 אלפיות השנייה או פחות. התוצאות האלה מאשרות את מה שציינו בהתחלה: WebAssembly מספק ביצועים חזויים. לא משנה באיזו שפה נבחר, ההבדלים בין הדפדפנים והשפות הם מינימליים. ליתר דיוק: סטיית התקן של JavaScript בכל הדפדפנים היא כ-400 אלפיות השנייה, ואילו סטיית התקן של כל המודולים שלנו ל-WebAssembly בכל הדפדפנים היא כ-80 אלפיות השנייה.

השקעת מאמץ

מדד נוסף הוא כמות המאמץ שנדרשה כדי ליצור את המודול של WebAssembly ולשלב אותו ב-squoosh. קשה להקצות ערך מספרי למאמץ, לכן לא אצור גרפים, אבל אציין כמה דברים:

העבודה עם AssemblyScript הייתה חלקה. הוא מאפשר להשתמש ב-TypeScript כדי לכתוב WebAssembly, וכך עוזר למהנדסי התוכנה שלנו לבדוק את הקוד בקלות. בנוסף, הוא יוצר מודולים של WebAssembly ללא דבק, שהם קטנים מאוד ועם ביצועים סבירים. סביר להניח שהכלים בסביבת TypeScript, כמו prettier ו-tslint, יפעלו כרגיל.

Rust בשילוב עם wasm-pack הוא גם נוח מאוד, אבל מתאים יותר לפרויקטים גדולים יותר של WebAssembly שבהם נדרשים קישורים וניהול זיכרון. היינו צריכים לחרוג מעט מהנתיב המאושר כדי להגיע לגודל קובץ תחרותי.

באמצעות C ו-Emscripten, נוצר מודול WebAssembly קטן מאוד עם ביצועים גבוהים מאוד, אבל בלי האומץ לקפוץ לקוד הדבקה ולצמצם אותו לצרכים הבסיסיים, הגודל הכולל (מודול WebAssembly + קוד הדבקה) מסתיים בגודל גדול למדי.

סיכום

אז באיזו שפה כדאי להשתמש אם יש לכם נתיב חם ב-JS ואתם רוצים שהוא יהיה מהיר יותר או עקבי יותר עם WebAssembly? כמו תמיד בשאלות לגבי ביצועים, התשובה היא: תלוי. אז מה השקנו?

תרשים השוואה

בהשוואה בין השפות השונות שבהן השתמשנו מבחינת הגודל של המודול לעומת הביצועים, נראה שהבחירה הטובה ביותר היא C או AssemblyScript. החלטנו לשלוח את Rust. יש כמה סיבות להחלטה הזו: כל הקודקים ששולחים ב-Squoosh עד כה עוברים הידור באמצעות Emscripten. רצינו להרחיב את הידע שלנו על הסביבה העסקית של WebAssembly ולהשתמש בשפה אחרת בסביבת הייצור. AssemblyScript הוא חלופה חזקה, אבל הפרויקט צעיר יחסית והמהדר (compiler) לא בוגר כמו המהדר של Rust.

ההבדל בגודל הקובץ בין Rust לבין השפות האחרות נראה די משמעותי בתרשים הפיזור, אבל הוא לא גדול במיוחד: טעינה של 500 מיליארד או 1.6KB אפילו יותר מ-2G לוקחת פחות מ-1/10 שנייה. ואנחנו מקווים ש-Rust יסגור את הפער בגודל המודול בקרוב.

מבחינת הביצועים בזמן הריצה, ל-Rust יש ממוצע מהיר יותר בדפדפנים שונים מאשר ב-AssemblyScript. במיוחד בפרויקטים גדולים יותר, סביר יותר ש-Rust יפיק קוד מהיר יותר בלי צורך באופטימיזציה ידנית של הקוד. אבל זה לא צריך למנוע ממך להשתמש במה שנוח לך.

עם זאת, AssemblyScript היה גילוי נהדר. היא מאפשרת למפתחי אינטרנט ליצור מודולים של WebAssembly בלי ללמוד שפה חדשה. הצוות של AssemblyScript היה זמין מאוד ועבד באופן פעיל על שיפור כלי הפיתוח שלו. בהחלט נמשיך לעקוב אחר AssemblyScript בעתיד.

עדכון: חלודה

אחרי שפרסם את המאמר הזה, ניק פיצג'רלד מצוות Rust הפנה אותנו אל הספר המעולה שלו, Rust Wasm, שמכיל קטע על אופטימיזציה של גודל קובץ. בעקבות ההוראות שם (במיוחד הפעלת אופטימיזציות בזמן קישור וטיפול ידני בסטרס) הצלחנו לכתוב קוד Rust "רגיל" ולחזור להשתמש ב-Cargo (ה-npm של Rust) בלי להגדיל את גודל הקובץ. מודול Rust מסתיים ב-370B אחרי gzip. לפרטים נוספים, אפשר לעיין בבקשת התיקון (PR) שפתחתי ב-Squoosh.

תודה מיוחדת לאשלי ויליאמס, סטיב קלבניק, ניק פיצג'רלד ומקס גריי על כל העזרה שלהם במסע הזה.