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

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

סורמה
סורמה

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

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

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

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

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

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

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

שרשור עם עובדי אינטרנט

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

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

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

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

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

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
  // ...
});

כדי לשלוח הודעה בחזרה ל-thread הראשי, צריך להשתמש באותו API של postMessage ב-Web worker ולהגדיר event listener ב-thread הראשי:

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);
});

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

מביאים בחשבון את הפשרות

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

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

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

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

מסכם

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

כמו כן, ל-OMT יש יתרונות משניים:

  • היא מעבירה את עלויות הביצוע של JavaScript לשרשור נפרד.
  • היא מעבירה ניתוח של עלויות, כלומר יכול להיות שממשק המשתמש יופעל מהר יותר. המצב הזה עשוי להפחית את הערך של הצגת תוכן ראשוני (FCP) או אפילו את הזמן עד לפעילות מלאה, וכתוצאה מכך יש סיכוי גבוה יותר את הציון של Lighthouse.

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

תמונה ראשית (Hero) מ-Unense, מאת James Peacock.