ארכיטקטורה של off-main-thread יכולה לשפר באופן משמעותי את המהימנות של האפליקציה ואת חוויית המשתמש.
ב-20 השנים האחרונות, האינטרנט התפתח באופן דרמטי ממסמכים סטטיים עם כמה סגנונות ותמונות, לאפליקציות מורכבות ודינמיות. עם זאת, דבר אחד נשאר ללא שינוי: יש לנו רק שרשור אחד לכל כרטיסיית דפדפן (עם כמה יוצאים מן הכלל) כדי לבצע את עבודת העיבוד של האתרים שלנו והרצת ה-JavaScript.
כתוצאה מכך, העומס על ה-thread הראשי גדל מאוד. ככל שאפליקציות האינטרנט הופכות מורכבות יותר, ה-main thread הופך לצוואר בקבוק משמעותי בביצועים. מה שגרוע יותר הוא שמשך הזמן שנדרש להפעלת קוד ב-thread הראשי עבור משתמש נתון הוא כמעט בלתי צפוי לחלוטין, כי ליכולות המכשיר יש השפעה עצומה על הביצועים. חוסר היכולת לחזות את הביצועים רק ילך ויגדל ככל שהמשתמשים יגשו לאינטרנט ממגוון רחב יותר של מכשירים, החל מטלפונים פשוטים עם מגבלות קיצוניות ועד למכונות דגל חזקות עם קצב רענון גבוה.
אם אנחנו רוצים שאפליקציות אינטרנט מתוחכמות יעמדו באופן מהימן בהנחיות לביצועים כמו Core Web Vitals (מדדים בסיסיים של חוויית המשתמש), שמבוססים על נתונים אמפיריים לגבי התפיסה והפסיכולוגיה האנושית, אנחנו צריכים דרכים להריץ את הקוד מחוץ לשרשור הראשי (OMT).
למה כדאי להשתמש ב-Web Workers?
כברירת מחדל, JavaScript היא שפה עם thread יחיד שמריצה משימות בthread הראשי. עם זאת, web workers מספקים מעין פתרון עקיפה ל-thread הראשי, בכך שהם מאפשרים למפתחים ליצור threads נפרדים לטיפול בעבודה מחוץ ל-thread הראשי. ההיקף של web workers מוגבל והם לא מאפשרים גישה ישירה ל-DOM, אבל הם יכולים להיות מאוד שימושיים אם יש הרבה עבודה שצריך לבצע, שאחרת תעמיס יתר על המאגר הראשי.
בנוגע למדדי הליבה לבדיקת חוויית המשתמש באתר, הפעלת עבודה מחוץ לשרשור הראשי יכולה להיות מועילה. בפרט, העברת עבודה מהשרשור הראשי לעובדי אינטרנט יכולה להפחית את התחרות על השרשור הראשי, וכך לשפר את מדד הרספונסיביות של הדף מהירות התגובה לאינטראקציה באתר (INP). כשיש פחות עבודה לעבד בשרשור הראשי, הוא יכול להגיב מהר יותר לאינטראקציות של המשתמשים.
צמצום העבודה ב-thread הראשי – במיוחד במהלך ההפעלה – יכול גם להועיל ל-Largest Contentful Paint (LCP) על ידי צמצום המשימות הארוכות. עיבוד של רכיב LCP דורש זמן בשרשור הראשי – לעיבוד של טקסט או תמונות, שהם רכיבי LCP נפוצים. אם מצמצמים את העבודה בשרשור הראשי, הסיכוי שרכיב ה-LCP של הדף ייחסם בגלל עבודה שדורשת הרבה משאבים ושאפשר לבצע באמצעות Web Worker קטן יותר.
שימוש בשרשורים עם web workers
בפלטפורמות אחרות יש בדרך כלל תמיכה בעבודה מקבילה, שמאפשרת להקצות פונקציה לשרשור, והיא פועלת במקביל לשאר התוכנית. אפשר לגשת לאותם משתנים משני השרשורים, והגישה למשאבים המשותפים האלה יכולה להיות מסונכרנת עם mutexes ו-semaphores כדי למנוע מרוץ תהליכים.
ב-JavaScript, אפשר לקבל פונקציונליות דומה בערך מ-web workers, שקיימים מאז 2007 ונתמכים בכל הדפדפנים העיקריים מאז 2012. ה-Web Workers פועלים במקביל לשרשור הראשי, אבל בניגוד לשרשור במערכת ההפעלה, הם לא יכולים לשתף משתנים.
כדי ליצור Web Worker, מעבירים קובץ לבונה של ה-Worker, שמתחיל להריץ את הקובץ הזה בשרשור נפרד:
const worker = new Worker("./worker.js");
כדי ליצור קשר עם ה-Web Worker, שולחים הודעות באמצעות postMessage API. מעבירים את ערך ההודעה כפרמטר בקריאה postMessage ואז מוסיפים מאזין לאירועי הודעות ל-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 ומגדירים מאזין אירועים בשרשור הראשי:
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 worker יחיד הופך במהירות למסורבל: צריך לקודד לא רק את הפרמטרים אלא גם את הפעולה בהודעה, וצריך לנהל רישום כדי להתאים בין התגובות לבקשות. המורכבות הזו היא כנראה הסיבה לכך ש-Web Workers לא אומצו באופן נרחב יותר.
אבל אם נוכל להקל על התקשורת בין ה-thread הראשי לבין web workers, המודל הזה יוכל להתאים להרבה תרחישי שימוש. למזלנו, יש ספרייה שעושה בדיוק את זה!
Comlink: מפחית את העבודה של עובדי אינטרנט
Comlink היא ספריה שמטרתה לאפשר לכם להשתמש ב-Web Workers בלי שתצטרכו לחשוב על הפרטים של postMessage. Comlink מאפשר לכם לשתף משתנים בין web workers לבין ה-main 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 בשרשור הראשי מתנהג כמו המשתנה ב-Web Worker, רק שכל פונקציה מחזירה הבטחה לערך ולא את הערך עצמו.
איזה קוד צריך להעביר ל-Web Worker?
ל-Web workers אין גישה ל-DOM ולממשקי API רבים כמו WebUSB, WebRTC או Web Audio, ולכן אי אפשר להוסיף ל-worker חלקים מהאפליקציה שמסתמכים על גישה כזו. עם זאת, כל קטע קוד קטן שמועבר ל-Worker מאפשר יותר מקום ב-Main Thread לפעולות שחייבות להתבצע שם, כמו עדכון ממשק המשתמש.
בעיה אחת שמפתחי אתרים נתקלים בה היא שרוב אפליקציות האינטרנט מסתמכות על מסגרת ממשק משתמש כמו Vue או React כדי לתזמן את כל הפעולות באפליקציה. כל הפעולות הן רכיב של המסגרת, ולכן הן קשורות באופן מובנה ל-DOM. לכן, נראה שקשה לבצע מיגרציה לארכיטקטורת OMT.
עם זאת, אם נעבור למודל שבו בעיות בממשק המשתמש מופרדות מבעיות אחרות, כמו ניהול מצב, עובדי אינטרנט יכולים להיות שימושיים מאוד גם באפליקציות שמבוססות על מסגרות. זו בדיוק הגישה שבה השתמשנו ב-PROXX.
PROXX: מקרה לדוגמה של OMT
צוות Google Chrome פיתח את PROXX כשיבוט של המשחק שולה מוקשים, שעומד בדרישות של אפליקציית Progressive Web App, כולל אפשרות לשחק במצב אופליין וחוויית משתמש מעניינת. לצערנו, הביצועים של גרסאות מוקדמות של המשחק במכשירים מוגבלים כמו טלפונים פשוטים היו נמוכים, ולכן הצוות הבין שההגבלה היא בשרשור הראשי.
הצוות החליט להשתמש ב-Web Workers כדי להפריד בין המצב החזותי של המשחק לבין הלוגיקה שלו:
- ה-thread הראשי מטפל ברינדור של אנימציות ומעברים.
- ה-Web Worker מטפל בלוגיקה של המשחק, שהיא חישובית בלבד.
ל-OMT היו השפעות מעניינות על הביצועים של PROXX בטלפונים פשוטים. בגרסה שאינה OMT, ממשק המשתמש קופא למשך שש שניות אחרי שהמשתמש מקיים איתו אינטראקציה. אין משוב, והמשתמש צריך לחכות את כל שש השניות לפני שהוא יכול לעשות משהו אחר.
לעומת זאת, בגרסת ה-OMT, המשחק לוקח 12 שניות כדי להשלים עדכון של ממשק המשתמש. למרות שזה נראה כאילו יש ירידה בביצועים, בפועל זה מוביל ליותר משוב למשתמש. ההאטה מתרחשת כי האפליקציה שולחת יותר פריימים מהגרסה שלא מבוססת על OMT, שלא שולחת פריימים בכלל. כך המשתמש יודע שמשהו קורה ויכול להמשיך לשחק בזמן שממשק המשתמש מתעדכן, מה שמשפר משמעותית את חוויית המשחק.
זהו פשרה מודעת: אנחנו מעניקים למשתמשים במכשירים מוגבלים חוויה שמרגישה טובה יותר, בלי לפגוע במשתמשים במכשירים מתקדמים.
השלכות של ארכיטקטורת OMT
כפי שאפשר לראות בדוגמה של PROXX, OMT מאפשרת להפעיל את האפליקציה באופן מהימן במגוון רחב יותר של מכשירים, אבל היא לא משפרת את מהירות האפליקציה:
- אתם רק מעבירים את העבודה מה-thread הראשי, ולא מצמצמים את העבודה.
- התקורה הנוספת של התקשורת בין ה-Web Worker לבין השרשור הראשי יכולה לפעמים להאט את התהליך.
התחשבות בהשפעות
מכיוון שה-thread הראשי פנוי לעיבוד אינטראקציות של משתמשים כמו גלילה בזמן ש-JavaScript פועל, יש פחות פריימים שנפסלים, גם אם זמן ההמתנה הכולל עשוי להיות ארוך יותר. עדיף לגרום למשתמש להמתין קצת מאשר להשמיט פריים, כי שולי הטעות קטנים יותר בהשמטת פריים: השמטת פריים מתרחשת באלפיות השנייה, בעוד שיש מאות אלפיות השנייה לפני שהמשתמש תופס את זמן ההמתנה.
בגלל חוסר היכולת לחזות את הביצועים במכשירים שונים, המטרה של ארכיטקטורת OMT היא לצמצם את הסיכון – להפוך את האפליקציה לחזקה יותר מול תנאי זמן ריצה משתנים מאוד – ולא לשפר את הביצועים באמצעות מקביליות. העלייה בעמידות והשיפורים בחוויית המשתמש שווים יותר מכל פשרה קטנה במהירות.
הערה לגבי כלים
טכנולוגיית Web workers עדיין לא נפוצה, ולכן רוב כלי המודולים – כמו webpack ו-Rollup – לא תומכים בהם מחוץ לקופסה. (אבל Parcel כן!) למזלנו, יש פלאגינים שמאפשרים ל-Web Workers לעבוד עם webpack ו-Rollup:
- worker-plugin ל-webpack
- rollup-plugin-off-main-thread ל-Rollup
סיכום
כדי לוודא שהאפליקציות שלנו אמינות ונגישות ככל האפשר, במיוחד בשוק גלובלי יותר ויותר, אנחנו צריכים לתמוך במכשירים מוגבלים – רוב המשתמשים בעולם ניגשים לאינטרנט דרך מכשירים כאלה. הטכנולוגיה הזו מציעה דרך מבטיחה לשפר את הביצועים במכשירים כאלה בלי להשפיע לרעה על משתמשים במכשירים מתקדמים.
בנוסף, יש ל-OMT יתרונות משניים:
- היא מעבירה את עלויות הביצוע של JavaScript ל-thread נפרד.
- הוא מעביר את עלויות הניתוח, כלומר יכול להיות שהממשק יופעל מהר יותר. יכול להיות שהפעולה הזו תפחית את הערך של הצגת תוכן ראשוני (FCP) או אפילו של הזמן עד שהאתר אינטראקטיבי (TTI), וכתוצאה מכך תעלה את הציון שלכם ב-Lighthouse.
אין סיבה לפחד מ-Web Workers. כלים כמו Comlink מפשטים את העבודה עם workers והופכים אותם לאפשרות מעשית למגוון רחב של אפליקציות אינטרנט.