זיהוי תכונות WebAssembly

למדו איך להשתמש בתכונות החדשות ביותר של WebAssembly ובמקביל לתמוך במשתמשים בכל הדפדפנים.

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

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

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

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

תכונות של בחירה וקיבוץ

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

טבלה שמציגה תמיכה של הדפדפן בתכונות שנבחרו.
ניתן לראות את טבלת התכונות הזו בכתובת webassembly.org/roadmap.

ניתן לפצל דפדפנים לקבוצות הבאות בעלות מאפיינים משותפים כדי להבטיח שכל משתמש יקבל את החוויה האופטימלית:

  • דפדפנים מבוססי Chrome: כולם תומכים בשרשורים, ב-SIMD ובטיפול בחריגים.
  • Firefox: תמיכה בפרוטוקול Thread ו-SIMD, אך טיפול בחריגות לא נתמך.
  • Safari: שרשורים נתמכים, SIMD וטיפול בחריגים לא נתמכים.
  • דפדפנים אחרים: מניחים רק תמיכה בסיסית ב-WebAssembly.

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

עיבוד עבור קבוצות תכונות שונות

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

כל שרשרת כלים ומערכת build היא שונה, ותצטרך לעיין בתיעוד של המהדר שלך כדי לדעת כיצד לשנות את התכונות האלה. כדי לשמור על הפשטות, בדוגמה הבאה אשתמש בספריית C++ עם קובץ יחיד ואראה איך להדר אותה באמצעות Emscripten.

אני אשתמש ב-SIMD דרך אמולציה של SSE2, בשרשורים דרך התמיכה בספריית Pthreads, ואבחר בין טיפול בחריג של Wasm לבין הטמעת JavaScript חלופית:

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

קוד C++ עצמו יכול להשתמש ב-#ifdef __EMSCRIPTEN_PTHREADS__ וב-#ifdef __SSE2__ כדי לבחור באופן מותנה בין הטמעות מקבילות (threads ו-SIMD) של אותן הפונקציות לבין ההטמעות הטוריות בזמן ההידור. כך הוא אמור להיראות:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

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

החבילה הנכונה בטעינה

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

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

מילים סופיות

בפוסט הזה הסברתי איך לבחור חבילות, לבנות אותן ולעבור ביניהן עבור קבוצות תכונות שונות.

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

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