טעינה של משאבי JavaScript גדולים משפיעה באופן משמעותי על מהירות הדף. פיצול קוד ה-JavaScript לחלקים קטנים יותר והורדה רק של מה שדרוש כדי שהדף יפעל במהלך ההפעלה יכולים לשפר מאוד את המהירות שבה הדף נטען, מה שיכול לשפר את האינטראקציה עם הרכיב הבא שייצבע (INP) בדף.
במהלך ההורדה, הניתוח והקומפילציה של קובצי JavaScript גדולים בדף, הדף עשוי להפסיק להגיב למשך פרקי זמן מסוימים. אלמנטים בדף גלויים, כי הם חלק מה-HTML הראשוני של הדף ומעוצבים על ידי CSS. עם זאת, יכול להיות שקוד ה-JavaScript שנדרש להפעלת האלמנטים האינטראקטיביים האלה – וגם סקריפטים אחרים שנטענים על ידי הדף – עובר ניתוח ומופעל כדי שהם יפעלו. התוצאה היא שהמשתמש עלול להרגיש שהאינטראקציה התעכבה באופן משמעותי, או אפילו נכשלה לחלוטין.
זה קורה לעיתים קרובות כי ה-thread הראשי נחסם, כי קוד ה-JavaScript מנותח ומקומפל ב-thread הראשי. אם התהליך הזה נמשך יותר מדי זמן, יכול להיות שרכיבים אינטראקטיביים בדף לא יגיבו מספיק מהר לקלט של המשתמש. אחת הדרכים לפתור את הבעיה היא לטעון רק את קוד ה-JavaScript שדרוש כדי שהדף יפעל, ולדחות את הטעינה של קוד JavaScript אחר למועד מאוחר יותר באמצעות טכניקה שנקראת פיצול קוד. במודול הזה נתמקד בטכניקה השנייה.
הפחתת הניתוח והביצוע של JavaScript במהלך ההפעלה באמצעות פיצול קוד
Lighthouse מציג אזהרה כשהביצוע של JavaScript נמשך יותר מ-2 שניות, ונכשל כשהוא נמשך יותר מ-3.5 שניות. ניתוח וביצוע מוגזמים של JavaScript הם בעיה פוטנציאלית בכל נקודה במחזור החיים של הדף, כי הם עלולים להגדיל את השהיה לאחר קלט ראשוני של אינטראקציה, אם הזמן שבו המשתמש מקיים אינטראקציה עם הדף חופף לרגע שבו משימות ה-thread הראשי שאחראיות לעיבוד ולביצוע של JavaScript פועלות.
בנוסף, ביצוע וניתוח מוגזמים של JavaScript בעייתיים במיוחד במהלך הטעינה הראשונית של הדף, כי זה השלב במחזור החיים של הדף שבו סביר מאוד שהמשתמשים יבצעו אינטראקציה עם הדף. למעשה, הזמן הכולל לחסימה (TBT) – מדד של רספונסיביות לטעינה – קשור מאוד ל-INP, מה שמצביע על כך שלמשתמשים יש נטייה גבוהה לנסות אינטראקציות במהלך הטעינה הראשונית של הדף.
הביקורת של Lighthouse שמדווחת על הזמן שנדרש להפעלת כל קובץ JavaScript שהדף מבקש, שימושית כי היא יכולה לעזור לכם לזהות בדיוק אילו סקריפטים יכולים להתאים לפיצול קוד. אפשר להשתמש בכלי הכיסוי ב-Chrome DevTools כדי לזהות בדיוק אילו חלקים של JavaScript בדף לא נמצאים בשימוש במהלך טעינת הדף.
פיצול קוד הוא טכניקה שימושית שיכולה לצמצם את המטען הייעודי (payload) הראשוני של JavaScript בדף. הוא מאפשר לפצל חבילת JavaScript לשני חלקים:
- קוד ה-JavaScript נדרש בזמן טעינת הדף, ולכן אי אפשר לטעון אותו בזמן אחר.
- קוד JavaScript שנותר וניתן לטעון אותו בשלב מאוחר יותר, בדרך כלל בשלב שבו המשתמש מקיים אינטראקציה עם אלמנט אינטראקטיבי מסוים בדף.
אפשר לבצע פיצול קוד באמצעות התחביר dynamic import()
. התחביר הזה – בניגוד לרכיבי <script>
שמבקשים משאב JavaScript נתון במהלך ההפעלה – שולח בקשה למשאב JavaScript בשלב מאוחר יותר במהלך מחזור החיים של הדף.
document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
// Get the form validation named export from the module through destructuring:
const { validateForm } = await import('/validate-form.mjs');
// Validate the form:
validateForm();
}, { once: true });
בקטע הקוד של JavaScript שמופיע למעלה, המודול validate-form.mjs
מורד, מנותח ומופעל רק כשמשתמש מבטל את הסימון של אחד מהשדות בטופס <input>
. במצב הזה, משאב ה-JavaScript שאחראי על לוגיקת האימות של הטופס מעורב בדף רק כשהכי סביר שייעשה בו שימוש בפועל.
אפשר להגדיר חבילות JavaScript כמו webpack, Parcel, Rollup ו-esbuild כך שיפצלו חבילות JavaScript לחלקים קטנים יותר בכל פעם שהן נתקלות בקריאה דינמית import()
בקוד המקור. רוב הכלים האלה עושים את זה באופן אוטומטי, אבל כדי להשתמש ב-esbuild צריך להפעיל את האופטימיזציה הזו.
הערות מועילות על פיצול קוד
פיצול קוד הוא שיטה יעילה לצמצום התחרות על השרשור הראשי במהלך הטעינה הראשונית של הדף, אבל אם מחליטים לבדוק את קוד המקור של JavaScript כדי למצוא הזדמנויות לפיצול קוד, כדאי לזכור כמה דברים.
מומלץ להשתמש בכלי לאריזת קבצים
בדרך כלל, מפתחים משתמשים במודולים של JavaScript במהלך תהליך הפיתוח. זהו שיפור מצוין בחוויית הפיתוח, שמשפר את קריאות הקוד ואת יכולת התחזוקה שלו. עם זאת, יש מאפייני ביצועים לא אופטימליים שיכולים להיווצר כששולחים מודולים של JavaScript לסביבת ייצור.
הכי חשוב להשתמש בכלי לאיגוד קבצים כדי לעבד ולבצע אופטימיזציה של קוד המקור, כולל מודולים שאתם מתכוונים לפצל את הקוד שלהם. כלי Bundler יעילים מאוד לא רק ביישום אופטימיזציות בקוד המקור של JavaScript, אלא גם באיזון בין שיקולי ביצועים כמו גודל החבילה לבין יחס הדחיסה. יעילות הדחיסה עולה ככל שגודל החבילה גדל, אבל כלי ה-bundling גם מנסים לוודא שהחבילות לא גדולות מדי, כדי שלא ייווצרו משימות ארוכות בגלל הערכת הסקריפט.
בנוסף, כלי ה-Bundler פותרים את הבעיה של שליחת מספר גדול של מודולים לא מאוגדים ברשת. בארכיטקטורות שמשתמשות במודולים של JavaScript יש בדרך כלל עצים גדולים ומורכבים של מודולים. כשמבטלים את ה-bundling של עצי מודולים, כל מודול מייצג בקשת HTTP נפרדת, והאינטראקטיביות באפליקציית האינטרנט עלולה להתעכב אם לא מבצעים bundling של מודולים. אפשר להשתמש ברמז המשאב <link rel="modulepreload">
כדי לטעון עצים גדולים של מודולים מוקדם ככל האפשר, אבל חבילות JavaScript עדיין עדיפות מבחינת ביצועי הטעינה.
איך מוודאים שלא משביתים בטעות את ההידור של סטרימינג
מנוע JavaScript V8 של Chromium מציע מספר אופטימיזציות מוכנות מראש כדי לוודא שקוד JavaScript של סביבת הייצור נטען בצורה יעילה ככל האפשר. אחד מהאופטימיזציות האלה נקרא קומפילציה של סטרימינג. כמו ניתוח ה-HTML המצטבר שמוזרם לדפדפן, הקומפילציה הזו מקמפלת נתחים של JavaScript שמוזרמים כשהם מגיעים מהרשת.
יש כמה דרכים לוודא שהקומפילציה של הסטרימינג מתבצעת באפליקציית האינטרנט שלכם ב-Chromium:
- משנים את קוד הייצור כדי להימנע משימוש במודולי JavaScript. חבילות יכולות לשנות את קוד המקור של JavaScript על סמך יעד קומפילציה, והיעד הוא לרוב ספציפי לסביבה נתונה. מנוע V8 יחיל קומפילציה של סטרימינג על כל קוד JavaScript שלא משתמש במודולים, ואפשר להגדיר את כלי האריזה כך שימיר את קוד מודול JavaScript לתחביר שלא משתמש במודולים של JavaScript ובתכונות שלהם.
- אם רוצים לשלוח מודולים של JavaScript לייצור, משתמשים בתוסף
.mjs
. לא משנה אם קוד ה-JavaScript שלכם משתמש במודולים או לא, אין סוג תוכן מיוחד ל-JavaScript שמשתמש במודולים לעומת JavaScript שלא משתמש במודולים. בנוגע ל-V8, אתם למעשה מבטלים את ההסכמה להזרמת קומפילציה כשאתם שולחים מודולים של JavaScript בייצור באמצעות התוסף.js
. אם משתמשים בתוסף.mjs
למודולים של JavaScript, V8 יכול לוודא שהקומפילציה של סטרימינג לקוד JavaScript מבוסס-מודולים לא נשברת.
אל תתנו לשיקולים האלה להרתיע אתכם משימוש בפיצול קוד. פיצול קוד הוא דרך יעילה לצמצם את המטען הייעודי (payload) הראשוני של JavaScript למשתמשים, אבל אם משתמשים ב-bundler ויודעים איך לשמר את התנהגות ההידור (compilation) של V8 בסטרימינג, אפשר לוודא שקוד ה-JavaScript שלכם בייצור יהיה מהיר ככל האפשר עבור המשתמשים.
הדגמה של ייבוא דינמי
webpack
webpack מגיע עם פלאגין בשם SplitChunksPlugin
, שמאפשר לכם להגדיר איך חבילת הקבצים מפצלת קובצי JavaScript. webpack מזהה את ההצהרות הדינמיות import()
והסטטיות import
. אפשר לשנות את אופן הפעולה של SplitChunksPlugin
על ידי ציון האפשרות chunks
בהגדרה שלו:
- ערך ברירת המחדל הוא
chunks: async
, והוא מתייחס לקריאות דינמיות שלimport()
. -
chunks: initial
מתייחס לשיחות סטטיות שלimport
. -
chunks: all
כולל ייבוא דינמיimport()
וייבוא סטטי, ומאפשר לכם לשתף נתחים בין ייבוא שלasync
לבין ייבוא שלinitial
.
כברירת מחדל, בכל פעם ש-webpack נתקל בהצהרת import()
דינמית, הוא יוצר נתח נפרד עבור המודול הזה:
/* main.js */
// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';
myFunction('Hello world!');
// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
// Assumes top-level await is available. More info:
// https://v8.dev/features/top-level-await
await import('/form-validation.js');
}
הגדרת ברירת המחדל של webpack לקטע הקוד שלמעלה יוצרת שני צ'אנקים נפרדים:
- chunk
main.js
– webpack מסווג אותו כ-chunkinitial
, והוא כולל את המודוליםmain.js
ו-./my-function.js
. - החלק
async
, שכולל רקform-validation.js
(שמכיל גיבוב קובץ בשם המשאב אם הוא מוגדר). החלק הזה יורד רק אםcondition
הוא truthy.
ההגדרה הזו מאפשרת לדחות את הטעינה של החלק form-validation.js
עד שהוא באמת נחוץ. הפעולה הזו יכולה לשפר את הרספונסיביות של הטעינה על ידי צמצום הזמן של הערכת הסקריפט במהלך הטעינה הראשונית של הדף. הורדה והערכה של סקריפט
עבור החלק form-validation.js
מתרחשות כשמתקיים תנאי מסוים. במקרה כזה, המודול שיובא באופן דינמי יורד. דוגמה אחת היא מצב שבו מתבצעת הורדה של polyfill רק לדפדפן מסוים, או – כמו בדוגמה הקודמת – המודול המיובא נחוץ לאינטראקציה של המשתמש.
לעומת זאת, שינוי ההגדרה של SplitChunksPlugin
לציון chunks: initial
מבטיח שהקוד יפוצל רק לחלקים הראשוניים. אלה נתחים כמו אלה שיובאו באופן סטטי, או שמופיעים במאפיין entry
של webpack. אם נסתכל על הדוגמה הקודמת, הנתח שיתקבל יהיה שילוב של form-validation.js
ושל main.js
בקובץ סקריפט יחיד, וכתוצאה מכך יכול להיות שביצועי הטעינה הראשונית של הדף יהיו גרועים יותר.
אפשר גם להגדיר את האפשרויות של SplitChunksPlugin
כך שיפצלו סקריפטים גדולים לכמה סקריפטים קטנים יותר. לדוגמה, אפשר להשתמש באפשרות maxSize
כדי להנחות את webpack לפצל חתיכות לקבצים נפרדים אם הן חורגות מהערך שצוין על ידי maxSize
. חלוקת קובצי סקריפט גדולים לקבצים קטנים יותר יכולה לשפר את מהירות הטעינה, כי במקרים מסוימים עבודת הערכת הסקריפט שדורשת הרבה משאבי CPU מחולקת למשימות קטנות יותר, שפחות סביר שיחסמו את השרשור הראשי לתקופות ארוכות יותר.
בנוסף, יצירה של קובצי JavaScript גדולים יותר גם אומרת שסביר יותר שהסקריפטים יסבלו מביטול תוקף של מטמון. לדוגמה, אם שולחים סקריפט גדול מאוד עם קוד של מסגרת וקוד של אפליקציה מצד ראשון, יכול להיות שהחבילה כולה תהיה לא תקפה אם רק המסגרת תעודכן, אבל שום דבר אחר במשאב המצורף לא יעודכן.
מצד שני, קובצי סקריפט קטנים מגדילים את הסיכוי שמבקר חוזר יאחזר משאבים מהמטמון, וכך טעינת הדפים תהיה מהירה יותר בביקורים חוזרים. עם זאת, דחיסה של קבצים קטנים לא מניבה שיפור משמעותי כמו דחיסה של קבצים גדולים, ויכולה להאריך את זמן הלוך ושוב ברשת בזמן טעינת דפים עם מטמון דפדפן לא מאותחל. חשוב לשמור על איזון בין יעילות השמירה במטמון, יעילות הדחיסה וזמן ההערכה של הסקריפט.
webpack demo
הדגמה של webpack SplitChunksPlugin
.
בוחנים את הידע
באיזה סוג של הצהרת import
משתמשים כשמבצעים פיצול קוד?
import()
.import
.
איזה סוג של הצהרת import
חייבת להופיע בחלק העליון של מודול JavaScript, ולא בשום מיקום אחר?
import()
.import
.
כשמשתמשים ב-SplitChunksPlugin
ב-webpack, מה ההבדל בין מקטע async
למקטע initial
?
async
chunks נטענים באמצעות import()
דינמי ו-initial
chunks נטענים באמצעות import
סטטי.
async
מתבצעת באמצעות import
סטטי, וטעינת מקטעי initial
מתבצעת באמצעות import()
דינמי.
הנושא הבא: טעינה מדורגת של תמונות ורכיבי <iframe>
למרות שמשאבי JavaScript הם בדרך כלל יקרים יחסית, הם לא סוג המשאב היחיד שאפשר לדחות את הטעינה שלו. רכיבי Image ו-<iframe>
הם משאבים שיכולים להיות יקרים בפני עצמם. בדומה ל-JavaScript, אפשר לדחות את הטעינה של תמונות ושל רכיב <iframe>
באמצעות טעינה עצלה, שמוסברת במודול הבא בקורס הזה.