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

הוא מהיר באופן עקבי, יו

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

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

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

הנתיב החם

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

הפונקציה הזו מבצעת איטרציה לכל פיקסל של תמונת קלט ומעתיקה אותה למיקום אחר בתמונת הפלט כדי ליצור סיבוב. לתמונה בגודל 4,094 על 4096 פיקסלים (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. המקבץ הוא פנימי לגמרי ב-VM, והוא לא נגיש למפתחי אתרים (למעט דרך כלי הפיתוח). לכן אפשר לכתוב מודולים של WebAssembly שלא צריכים בכלל זיכרון נוסף, ולהשתמש רק בסטאק הפנימי של ה-VM.

במקרה שלנו, נצטרך להשתמש בזיכרון נוסף כדי לאפשר גישה שרירותית לפיקסלים של התמונה וליצור גרסת סיבוב של התמונה הזו. זאת המטרה של 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 ומודול Wam שנקרא c.wasm. שימו לב שמודול Wam מכווץ את קובץ ה-gzip ל-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, אנחנו צריכים ליצור פרוסות שימוש בכתובות ההתחלה שלנו. פעולה זו נוגדת את מודל בטיחות הזיכרון ש-Rrust אוכפת, ולכן כדי שנוכל להשתמש במילת המפתח 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;
    }
}

מתבצע הידור של קובצי חלודה באמצעות

$ 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

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

טוויגי

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

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

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

וואאם-סטריפ

wasm-strip הוא כלי מ-WebAssembly Binary Toolkit, או בקיצור Wabt. הוא מכיל כמה כלים שמאפשרים לבדוק מודולים של WebAssembly ולשנות אותם. wasm2wat הוא כלי פירוק שהופך מודול Wam בינארי לפורמט קריא לאנשים. Wabt מכיל גם wat2wasm, שמאפשר להפוך את הפורמט הזה שקריא לבני אדם בחזרה למודול Wabt בינארי. אמנם השתמשנו בשני הכלים המשלימים האלה כדי לבדוק את קובצי ה-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

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

ביצועים

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

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

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

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

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

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

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

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

השקעת מאמץ

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

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

גם חלודה בשילוב עם 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 יש ממוצע מהיר יותר בדפדפנים שונים מאשר ב-AssemblyScript. יש סיכוי גבוה יותר ש-Rust יפיק קוד מהיר יותר, במיוחד בפרויקטים גדולים יותר בלי שיהיה צורך באופטימיזציה ידנית של הקוד. אבל זה לא צריך למנוע ממך להשתמש במה שנוח לך.

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

עדכון: חלודה

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

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