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

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

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

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

אם אנחנו רוצים שאפליקציות אינטרנט מתוחכמות יעמדו באופן מהימן בהנחיות הביצועים כמו מדדי הליבה של חוויית המשתמש (Core Web Vitals) – שמבוססים על נתונים אמפיריים לגבי התפיסה והפסיכולוגיה האנושית – אנחנו צריכים דרכים להריץ את הקוד שלנו מחוץ לשרשור הראשי (OMT).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

סיכום

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

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

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