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

ארכיטקטורה שפועלת מחוץ ל-main thread יכולה לשפר משמעותית את האמינות של האפליקציה ואת חוויית המשתמש.

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

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

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

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

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

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

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

תהליכים עם Web Workers

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

ב-JavaScript, אנחנו יכולים לקבל פונקציונליות דומה פחות או יותר מ-Web Workers, שקיימים מאז 2007 ונתמכים בכל הדפדפנים העיקריים מאז 2012. ה-Web Workers פועלים במקביל לשרשור הראשי, אבל בניגוד לשרשורים של מערכת ההפעלה, הם לא יכולים לשתף משתנים.

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

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

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

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 ומגדירים רכיב event listener בשרשור הראשי:

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

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

Comlink היא ספרייה שמאפשרת להשתמש ב-Web Workers בלי לחשוב על הפרטים של postMessage. בעזרת Comlink אפשר לשתף משתנים בין קובצי Web Worker לבין ה-thread הראשי, כמעט כמו בשפות תכנות אחרות שתומכות ב-threading.

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

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 ב-main thread פועל כמו המשתנה ב-web worker, אבל כל פונקציה מחזירה הבטחה לערך במקום את הערך עצמו.

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

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

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

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

‫PROXX: מקרה לדוגמה של OMT

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

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

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

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

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

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

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

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

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

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

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

היתרונות והחסרונות

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

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

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

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

סיכום

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

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

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