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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Safari ו-Firefox

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

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

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

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

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

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

סקריפטים שנטענים ב-web worker

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

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

יתרונות וחסרונות ושיקולים

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

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

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

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

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

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

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

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

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

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

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

סיכום

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

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

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

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