להשתמש ברכיבי אינטרנט כדי להריץ JavaScript מה-thread הראשי של הדפדפן

ארכיטקטורה מחוץ לשרשור הראשי יכולה לשפר באופן משמעותי את האמינות ואת חוויית המשתמש של האפליקציה.

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

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

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

למה כדאי להשתמש ב-Web Workers?

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

מבחינת מדדי הליבה לבדיקת חוויית המשתמש באתר, כדאי להריץ משימות מחוץ ל-thread הראשי. באופן ספציפי, העברת עבודה מהשרשור הראשי ל-Web Workers יכולה לצמצם את המאבק על השרשור הראשי, וכך לשפר את המדד אינטראקציה לצביעה הבאה (INP) של הדף. כאשר לשרשור הראשי יש פחות עבודה לעיבוד, הוא יכול להגיב מהר יותר לאינטראקציות של המשתמשים.

פחות עבודה ב-thread הראשי – במיוחד במהלך ההפעלה – יכולה גם להועיל לLargest Contentful Paint ‏ (LCP) על ידי הפחתת המשימות הארוכות. עיבוד של רכיב LCP דורש זמן ב-thread הראשי – לעיבוד טקסט או תמונות, שהם רכיבי LCP נפוצים – וככל שמפחיתים את העבודה הכוללת ב-thread הראשי, כך יש פחות סיכוי שרכיב ה-LCP של הדף ייחסם על ידי עבודה יקרה ש-web worker יכול לטפל בה במקום זאת.

שימוש בשרשור עם משימות אינטרנט

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

ב-JavaScript, אפשר לקבל פונקציונליות דומה בעזרת משימות אינטרנט (web workers), שהופיעו בשנת 2007 ונתמכות בכל הדפדפנים העיקריים מאז 2012. משימות ה-Web Worker פועלות במקביל לשרשור הראשי, אבל בניגוד לשרשור במערכת ההפעלה, הן לא יכולות לשתף משתנים.

כדי ליצור עובד אינטרנט, מעבירים קובץ למבנה ה-worker, שמתחיל להריץ את הקובץ הזה בשרשור נפרד:

const worker = new Worker("./worker.js");

אפשר לתקשר עם ה-Web Worker על ידי שליחת הודעות באמצעות postMessage API. מעבירים את ערך ההודעה כפרמטר בקריאה ל-postMessage, ואז מוסיפים לעובד מאזין לאירועי הודעות:

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

כדי לשלוח הודעה חזרה לשרשור הראשי, משתמשים באותו API של postMessage ב-web worker ומגדירים מאזין לאירועים בשרשור הראשי:

main.js

const worker = new Worker('./worker.js');

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

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

עם זאת, אם נוכל להסיר חלק מהקשיים בתקשורת בין הליבה לבין משימות ה-web worker, המודל הזה יכול להתאים היטב למקרים רבים של שימוש. למזלנו, יש ספרייה שעושה את כל זה!

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

כדי להגדיר את Comlink, מייבאים אותו ל-web worker ומגדירים קבוצת פונקציות שרוצים לחשוף ל-thread הראשי. לאחר מכן מייבאים את Comlink בשרשור הראשי, עוטפים את העובד ומקבלים גישה לפונקציות החשופות:

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

המשתנה api בשרשור הראשי מתנהג כמו המשתנה ב-web worker, חוץ מזה שכל פונקציה מחזירה הבטחה (promise) לערך ולא את הערך עצמו.

איזה קוד צריך להעביר ל-Web Worker?

לעובדי אינטרנט אין גישה ל-DOM ולממשקי API רבים כמו WebUSB, WebRTC או Web Audio, לכן לא ניתן להעביר ל-Worker ולראות חלקים מהאפליקציה שמסתמכים על גישה כזו. עם זאת, כל קטע קוד קטן שמועבר לעובד חוסך מקום בשרשור הראשי לדברים שחייבים להיות שם – כמו עדכון ממשק המשתמש.

אחת הבעיות של מפתחי אינטרנט היא שרוב אפליקציות האינטרנט מסתמכות על מסגרת UI כמו Vue או React כדי לתזמור את כל האלמנטים באפליקציה. כל אלמנט הוא רכיב של המסגרת, ולכן הוא קשור באופן מהותי ל-DOM. נראה שזה יקשה על המעבר לארכיטקטורה של OMT.

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

PROXX: מקרה לדוגמה בנושא ניהול תנועה אורגנית

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

הצוות החליט להשתמש ב-web workers כדי להפריד בין המצב החזותי של המשחק לבין הלוגיקה שלו:

  • ה-thread הראשי מטפל בעיבוד של אנימציות ומעברים.
  • עובד אינטרנט מטפל בלוגיקת משחק, שהיא חישובית בלבד.

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

זמן התגובה של ממשק המשתמש בגרסה שלא כוללת OMT של PROXX.

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

זמן התגובה של ממשק המשתמש בגרסה OMT של PROXX.

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

ההשלכות של ארכיטקטורת OMT

כפי שרואים בדוגמה של PROXX, OMT מאפשר לאפליקציה לפעול בצורה מהימנה במגוון רחב יותר של מכשירים, אבל הוא לא מאיץ את האפליקציה:

  • אתם רק מעבירים את העבודה מה-thread הראשי, ולא מפחיתים את העבודה.
  • לפעמים, העלות הנוספת של התקשורת בין ה-web worker לבין ה-thread הראשי יכולה להאט את התהליך במעט.

שקלול היתרונות והחסרונות

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

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

הערה לגבי כלים

עדיין אין שימוש נרחב ב-Web Workers, ולכן רוב הכלים למודולים – כמו webpack ו-Rollup – לא תומכים בהם מראש. (אבל Parcel כן תומך בכך). למרבה המזל, קיימים יישומי פלאגין שבעזרתם אפשר לעבוד באינטרנט, ובכן, לעבוד עם webpack ו-rollup:

סיכום

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

בנוסף, ל-OMT יש יתרונות משניים:

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