כשאתם טוענים סקריפטים, הדפדפן צריך זמן כדי להעריך אותם לפני ההפעלה, וזה עלול לגרום למשימות ארוכות. איך פועלת הערכת הסקריפטים, ומה אפשר לעשות כדי למנוע מהם לגרום למשימות ארוכות במהלך טעינת הדף?
כשמדובר באופטימיזציה של זמן האינטראקציה עד התוכן הבא (INP), רוב העצות שתקבלו יהיו בנוגע לאופטימיזציה של האינטראקציות עצמן. לדוגמה, במדריך לאופטימיזציה של משימות ארוכות מפורטות שיטות כמו העברה לטיפול אחר באמצעות setTimeout
ושיטות אחרות. הטכניקות האלה מועילות כי הן מאפשרות ל-thread הראשי קצת מרווח נשימה על ידי הימנעות ממשימות ארוכות. כך יש יותר הזדמנויות לאינטראקציות ולפעילות אחרת לפעול מוקדם יותר, במקום להמתין למשימת זמן ארוכה אחת.
עם זאת, מה קורה למשימות הארוכות שמגיעות מהטעינה של הסקריפטים עצמם? המשימות האלה עלולות להפריע לאינטראקציות של משתמשים ולהשפיע על מדד INP של הדף במהלך הטעינה. במדריך הזה נסביר איך הדפדפנים מטפלים במשימות שמתחילות בהערכת סקריפט, ונראה מה אפשר לעשות כדי לפצל את העבודה של הערכת הסקריפט, כדי שהשרשור הראשי יוכל להגיב מהר יותר לקלט של המשתמש בזמן טעינת הדף.
מהי הערכה של סקריפט?
אם יצרתם פרופיל של אפליקציה שמכילה הרבה JavaScript, יכול להיות שתראו משימות ארוכות שבהן הגורם לבעיה מסומן בתווית Evaluate Script.
הערכת הסקריפט היא חלק הכרחי בהפעלת JavaScript בדפדפן, כי קוד JavaScript מקובץ בדיוק בזמן לפני ההפעלה. כשסקריפט נבדק, קודם מתבצע ניתוח שלו כדי למצוא שגיאות. אם המנתח לא מוצא שגיאות, הסקריפט עובר הידור לקוד בייט, ואז אפשר להמשיך להרצה.
הערכת הסקריפט נחוצה, אבל היא עלולה להיות בעייתית כי משתמשים עשויים לנסות ליצור אינטראקציה עם דף זמן קצר אחרי שהוא עבר עיבוד ראשוני. עם זאת, גם אם הדף עבר עיבוד, לא בטוח שהדף הסתיים לטוען. אינטראקציות שמתרחשות במהלך הטעינה עשויות להתעכב כי הדף עסוק בהערכת סקריפטים. אין ערובה לאינטראקציה שיכולה להתרחש בשלב הזה – יכול להיות שהסקריפט שאחראי עליה עדיין לא נטען – אבל יכול להיות שיש אינטראקציות שתלוית ב-JavaScript ומוכנות, או שהאינטראקטיביות לא תלויה ב-JavaScript בכלל.
הקשר בין סקריפטים לבין המשימות שמעריכות אותם
האופן שבו מתחילות המשימות שאחראיות על הערכת הסקריפט תלוי בשאלה אם הסקריפט שאתם מעמיסים נטען עם אלמנט <script>
או אם הסקריפט הוא מודול שנטען עם type=module
. מאחר שדפדפנים נוטים לטפל בדברים בצורה שונה, נתייחס לאופן שבו מנועי הדפדפנים העיקריים מטפלים בהערכת סקריפטים, תוך התייחסות לפערים בהתנהגות שלהם בנושא הערכת סקריפטים.
סקריפטים שנטענים באמצעות הרכיב <script>
בדרך כלל, מספר המשימות שנשלחות להערכת סקריפטים קשור ישירות למספר האלמנטים מסוג <script>
בדף. כל רכיב <script>
מפעיל משימה להערכת הסקריפט המבוקש, כדי שניתן יהיה לנתח, לקמפל ולהריץ אותו. הדבר נכון לגבי דפדפנים מבוססי Chromium, Safari ו- Firefox.
למה זה חשוב? נניח שאתם משתמשים ב-bundler כדי לנהל את הסקריפטים בסביבת הייצור, והגדרתם אותו לארוז את כל מה שדרוש לדף כדי לפעול בסקריפט יחיד. אם זה המצב באתר שלכם, סביר להניח שתישלח משימה אחת כדי להעריך את הסקריפט הזה. האם זה דבר רע? לא בהכרח – אלא אם הסקריפט עצום.
כדי לפצל את עבודת הערכת הסקריפט, אפשר להימנע משימוש בחיבור של קטעי קוד JavaScript גדולים, ולהשתמש ברכיבי <script>
נוספים כדי לטעון סקריפטים קטנים יותר בנפרד.
תמיד כדאי לנסות לטעון כמה שפחות JavaScript במהלך טעינת הדף, אבל פיצול הסקריפטים מבטיח שבמקום משימה אחת גדולה שעלולה לחסום את הליבה, תהיה לכם כמות גדולה יותר של משימות קטנות יותר שלא יחסמו את הליבה בכלל – או לפחות פחות משימות ממה שהתחילתם.
אפשר לחשוב על פיצול המשימות לצורך הערכת סקריפט כפעולה דומה להחזרת ערכים במהלך קריאות חוזרות של אירועים שפועלות במהלך אינטראקציה. עם זאת, כשמבצעים הערכה של סקריפט, מנגנון ההעברה (yield) מפצל את קוד ה-JavaScript שאתם מעלים לכמה סקריפטים קטנים יותר, במקום למספר קטן יותר של סקריפטים גדולים יותר שיש סיכוי גבוה יותר שיחסמו את הליבה הראשית.
סקריפטים שנטענו עם הרכיב <script>
והמאפיין type=module
עכשיו אפשר לטעון מודולים של ES באופן מקורי בדפדפן באמצעות המאפיין type=module
באלמנט <script>
. הגישה הזו לטעינת סקריפטים מביאה כמה יתרונות לחוויית הפיתוח, למשל: אין צורך לבצע טרנספורמציה של קוד לשימוש בסביבת הייצור – במיוחד כשמשתמשים בה בשילוב עם מיפויי ייבוא. עם זאת, טעינת סקריפטים באופן הזה מתזמנת משימות שונות מדפדפן לדפדפן.
דפדפנים מבוססי Chromium
בדפדפנים כמו Chrome – או בדפדפנים שמבוססים עליו – טעינת מודולים של ES באמצעות המאפיין type=module
יוצרת סוגים שונים של משימות מאשר אלה שרואים בדרך כלל כשלא משתמשים ב-type=module
. לדוגמה, תופעל משימה לכל סקריפט של מודול שכוללת פעילות שמסומנת בתווית Compile module.
אחרי שהמודולים יקובצו, כל קוד שפועל בהם לאחר מכן יפעיל פעילות שמסומנת בתווית בדיקת המודול.
ההשפעה כאן – לפחות ב-Chrome ובדפדפנים קשורים – היא שהשלבים של הידור מחולקים כשמשתמשים במודולים של ES. זוהי יתרון ברור מבחינת ניהול משימות ארוכות, אבל עדיין תצטרכו להוציא על הערכת המודולים, כך שעדיין תצטרכו לשלם עלות מסוימת. מומלץ להשתדל לשלוח כמה שפחות JavaScript, אבל שימוש במודולים של ES – ללא קשר לדפדפן – מספק את היתרונות הבאים:
- כל קוד המודול מופעל באופן אוטומטי במצב קפדני, שמאפשר אופטימיזציות פוטנציאליות על ידי מנועי JavaScript שלא ניתן לבצע בהקשר לא קפדני.
- סקריפטים שנטענים באמצעות
type=module
מטופלים כברירת מחדל כאילו הם נדחו. אפשר להשתמש במאפייןasync
בסקריפטים שנטענים באמצעותtype=module
כדי לשנות את ההתנהגות הזו.
Safari ו-Firefox
כשמודולים נטענים ב-Safari וב-Firefox, כל אחד מהם נבדק במשימה נפרדת. כלומר, באופן תיאורטי אפשר לטעון מודול יחיד ברמה העליונה שמכיל רק הצהרות static import
למודולים אחרים, וכל מודול טעון יגרום לבקשת רשת ולמשימה נפרדות כדי להעריך אותו.
סקריפטים שנטענים באמצעות import()
דינמי
import()
דינמי היא שיטה נוספת לטעינה של סקריפטים. בניגוד להצהרות import
סטטיות שצריכות להופיע בחלק העליון של מודול ES, קריאה דינמית ל-import()
יכולה להופיע בכל מקום בסקריפט כדי לטעון מקטע של JavaScript על פי דרישה. הטכניקה הזו נקראת פיצול קוד.
ל-import()
דינמי יש שני יתרונות כשמדובר בשיפור INP:
- מודולים שנדחים לטעינה במועד מאוחר יותר מפחיתים את התחרות על ה-thread הראשי במהלך ההפעלה, על ידי צמצום כמות ה-JavaScript שנטען באותו זמן. כך אפשר לפנות את השרשור הראשי כדי שיהיה לו יותר זמן להגיב לאינטראקציות של המשתמשים.
- כשמתבצעות קריאות
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, וכך לספק חוויית משתמש טובה יותר.