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

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

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

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

מהי הערכת סקריפטים?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

דפדפנים המבוססים על Chromium

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

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

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

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

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

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

Safari ו-Firefox

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

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

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

לimport() הדינמי יש שני יתרונות לשיפור INP:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

סיכום

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

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

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

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

תמונה ראשית (Hero) מ-Unbounce, מאת Markus Spiske.