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

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

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

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

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

יש מבחר גדול

אם תעיינו בפונקציית JavaScript המקורית שאנחנו רוצים להמיר ל-WebAssembly, תוכלו לראות שמדובר בקוד חישובי בלבד ללא ממשקי 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, של קבוצת העבודה rustwasm. wasm-pack ממיר את הקוד שלכם למודול ידידותי לאינטרנט שפועל מתוך האריזה עם חבילות כמו 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;
    }
}

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

$ wasm-pack build

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

$ wasm-strip rotate_bg.wasm

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

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

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

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

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

עדכון: Rust

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

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