הערכת סקריפטים ומשימות ארוכות

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

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

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

מהי הערכה של סקריפט?

אם יצרתם פרופיל של אפליקציה שכוללת הרבה JavaScript, יכול להיות שראיתם משימות ארוכות שבהן הגורם הוא Evaluate Script (הערכת סקריפט).

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

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

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

הקשר בין סקריפטים לבין המשימות שמעריכות אותם

האופן שבו מופעלות משימות שאחראיות להערכת סקריפט תלוי בשאלה אם הסקריפט שאתם טוענים נטען באמצעות רכיב <script> טיפוסי, או אם הסקריפט הוא מודול שנטען באמצעות type=module. מכיוון שדפדפנים נוטים לטפל בדברים בצורה שונה, נסביר איך מנועי הדפדפנים העיקריים מטפלים בהערכת סקריפטים במקרים שבהם יש הבדלים בהתנהגות של הערכת סקריפטים בין המנועים.

סקריפטים שנטענו באמצעות הרכיב <script>

מספר המשימות שמוקצות להערכת סקריפטים קשור בדרך כלל באופן ישיר למספר הרכיבים <script> בדף. כל רכיב <script> מפעיל משימה להערכת הסקריפט המבוקש, כדי שאפשר יהיה לנתח, לקמפל ולהריץ אותו. זה המצב בדפדפנים מבוססי Chromium, ‏ Safari, ו Firefox.

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

כדי לחלק את העבודה של הערכת הסקריפט, כדאי להימנע מטעינה של נתחי JavaScript גדולים, ולטעון סקריפטים קטנים יותר באמצעות רכיבי <script> נוספים.

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

כמה משימות שכוללות הערכת סקריפט, כפי שמוצג בפרופיל הביצועים של הכלים למפתחים ב-Chrome. מכיוון שנטענים כמה סקריפטים קטנים במקום פחות סקריפטים גדולים, הסיכוי שמשימות יהפכו למשימות ארוכות קטן יותר, והשרשור הראשי יכול להגיב לקלט של משתמשים מהר יותר.
נוצרו כמה משימות להערכת סקריפטים כתוצאה מכמה רכיבי <script> שמופיעים ב-HTML של הדף. עדיף לשלוח למשתמשים חבילת סקריפטים קטנה יותר מאשר חבילת סקריפטים גדולה, כי יש סיכוי גבוה יותר שחבילת סקריפטים גדולה תחסום את ה-thread הראשי.

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

סקריפטים שנטענו באמצעות הרכיב <script> והמאפיין type=module

עכשיו אפשר לטעון מודולים של ES באופן מקורי בדפדפן באמצעות המאפיין type=module באלמנט <script>. לגישה הזו לטעינת סקריפטים יש כמה יתרונות מבחינת חוויית המפתחים, למשל, לא צריך לשנות את הקוד לשימוש בסביבת הייצור – במיוחד כשמשתמשים בה בשילוב עם מיפוי ייבוא. עם זאת, טעינת סקריפטים באופן הזה מתזמנת משימות ששונות מדפדפן לדפדפן.

דפדפנים מבוססי Chromium

בדפדפנים כמו Chrome – או בדפדפנים שנגזרים ממנו – טעינת מודולים של ES באמצעות המאפיין type=module יוצרת סוגים שונים של משימות מאלה שרואים בדרך כלל כשלא משתמשים ב-type=module. לדוגמה, תופעל משימה לכל סקריפט של מודול, שתכלול פעילות עם התווית Compile module.

עבודת קומפילציה של מודולים בכמה משימות, כפי שמוצג בכלי הפיתוח ל-Chrome.
התנהגות טעינת המודולים בדפדפנים מבוססי Chromium. כל סקריפט של מודול יפעיל קריאה ל-Compile module כדי לקמפל את התוכן שלו לפני ההערכה.

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

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

ההשפעה כאן – לפחות ב-Chrome ובדפדפנים קשורים – היא ששלבי ההידור מפוצלים כשמשתמשים במודולי ES. זהו יתרון ברור מבחינת ניהול משימות ארוכות, אבל עדיין תהיה לכם עלות בלתי נמנעת כתוצאה מהעבודה שנדרשת להערכת המודול. מומלץ לשלוח כמה שפחות JavaScript, אבל שימוש במודולי ES – ללא קשר לדפדפן – מספק את היתרונות הבאים:

  • כל קוד המודול מורץ באופן אוטומטי במצב קפדני, שמאפשר אופטימיזציות פוטנציאליות על ידי מנועי JavaScript שלא ניתן לבצע בדרך אחרת בהקשר לא קפדני.
  • סקריפטים שנטענים באמצעות type=module מטופלים כdeferred כברירת מחדל. אפשר להשתמש במאפיין async בסקריפטים שנטענים באמצעות type=module כדי לשנות את ההתנהגות הזו.

‫Safari ו-Firefox

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

סקריפטים שנטענו באמצעות תג import() דינמי

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

ל-import() דינמי יש שני יתרונות בכל הנוגע לשיפור מדד INP:

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

התנהגות של קריאות דינמיות import() דומה בכל מנועי הדפדפנים העיקריים: משימות ההערכה של הסקריפט שיתקבלו יהיו זהות למספר המודולים שיובאו באופן דינמי.

סקריפטים שנטענו ב-Web Worker

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

בנוסף לצמצום העבודה בשרשור הראשי, עובדי האינטרנט יכולים לטעון סקריפטים חיצוניים לשימוש בהקשר של העובד, באמצעות importScripts או הצהרות סטטיות של import בדפדפנים שתומכים בעובדי מודולים. התוצאה היא שכל סקריפט שנדרש על ידי Web Worker מוערך מחוץ לשרשור הראשי.

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

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

יעילות דחיסת הנתונים

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

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

  • במקרה של webpack, התוסף SplitChunksPlugin שלו יכול לעזור. אפשר לעיין במסמכי התיעוד של SplitChunksPlugin כדי לראות אילו אפשרויות זמינות לניהול הגודל של נכסים.
  • בתוכנות אחרות לאריזת קבצים, כמו Rollup ו-esbuild, אפשר לנהל את הגודל של קובצי הסקריפט באמצעות קריאות דינמיות ל-import() בקוד. הכלי הזה, כמו גם webpack, יפצל אוטומטית את הנכס שיובא באופן דינמי לקובץ משלו, וכך יימנע גודל חבילה ראשוני גדול יותר.

ביטול תוקף של מטמון

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

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

מודולים מוטמעים וביצועי טעינה

אם אתם שולחים מודולים של ES בייצור וטוענים אותם באמצעות המאפיין type=module, חשוב שתדעו איך קינון מודולים יכול להשפיע על זמן ההפעלה. המונח 'קינון מודולים' מתייחס למצב שבו מודול ES מייבא באופן סטטי מודול ES אחר, שמייבא באופן סטטי מודול ES אחר:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

אם מודולי ה-ES לא מאוגדים יחד, הקוד שלמעלה יוצר שרשרת של בקשות לאחזור מהרשת: כשמתבצעת בקשה לאחזור מהרשת ל-a.js מרכיב <script>, נשלחת בקשה לאחזור מהרשת נוספת ל-b.js, שכוללת בקשה לאחזור מהרשת נוספת ל-c.js. אחת הדרכים להימנע מכך היא להשתמש בכלי לאיגוד קבצים (bundler), אבל חשוב להגדיר את הכלי כך שהוא יפצל את הסקריפטים כדי לפזר את עבודת ההערכה של הסקריפטים.

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

סיכום

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

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

  • כשמעלים סקריפטים באמצעות רכיב <script> בלי המאפיין type=module, כדאי להימנע מהעלאת סקריפטים גדולים מאוד, כי הם יפעילו משימות הערכה של סקריפטים שצורכות הרבה משאבים וחוסמות את השרשור הראשי. כדי לפצל את העבודה הזו, כדאי לפרוס את הסקריפטים על פני יותר רכיבי <script>.
  • שימוש במאפיין type=module לטעינת מודולי ES באופן מקורי בדפדפן יפעיל משימות נפרדות להערכה של כל סקריפט מודול נפרד.
  • כדי להקטין את הגודל של חבילות ההורדה הראשוניות, אפשר להשתמש בקריאות דינמיות ל-import(). האפשרות הזו פועלת גם ב-bundlers, כי כל מודול שיובא באופן דינמי ייחשב כנקודת פיצול, וכתוצאה מכך ייווצר סקריפט נפרד לכל מודול שיובא באופן דינמי.
  • חשוב לשקול את הפשרות הנדרשות, כמו יעילות הדחיסה וביטול התוקף של מטמון. סקריפטים גדולים יותר יתכווצו טוב יותר, אבל סביר להניח שהם יכללו עבודת הערכה יקרה יותר של הסקריפט בפחות משימות, ויגרמו לביטול התוקף של מטמון הדפדפן, מה שיוביל ליעילות נמוכה יותר של שמירת נתונים במטמון.
  • אם משתמשים במודולי ES באופן מקורי בלי חבילה, כדאי להשתמש ברמז המשאב modulepreload כדי לבצע אופטימיזציה של הטעינה שלהם במהלך ההפעלה.
  • כמו תמיד, מומלץ לשלוח כמה שפחות JavaScript.

זהו איזון עדין, אבל באמצעות פיצול סקריפטים והפחתת המטען הייעודי (payload) הראשוני באמצעות import() דינמי, אפשר לשפר את ביצועי ההפעלה ולהתאים טוב יותר את האינטראקציות של המשתמשים במהלך תקופת ההפעלה הקריטית הזו. השינויים האלה אמורים לעזור לכם לקבל ציון טוב יותר במדד INP, וכך לספק חוויית משתמש טובה יותר.