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

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

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

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

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

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

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

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

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

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

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

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

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

פשרות ושיקולים

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

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

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

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

  • ב-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, וכך לספק חוויית משתמש טובה יותר.