פיצול קוד JavaScript

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

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

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

הפחתת הניתוח וההפעלה של JavaScript במהלך ההפעלה באמצעות פיצול קוד

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

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

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

פיצול קוד היא שיטה שימושית שיכולה לצמצם את קוד ה-JavaScript הראשוני של דף מטענים ייעודיים (payloads). היא מאפשרת לפצל חבילת JavaScript לשני חלקים:

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

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

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

הערות מועילות לגבי פיצול קוד

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

אם אפשר, כדאי להשתמש ב-bundler

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

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

יצרני חבילה מונעים גם את הבעיה של משלוח מספר גדול של מודולים לא ארוזים דרך הרשת. לארכיטקטורות שמשתמשות במודולים של JavaScript יש בדרך כלל עצי מודול מורכבים. כשעצי מודול לא מקובצים, כל מודול מייצג בקשת HTTP נפרדת, והאינטראקטיביות באפליקציית האינטרנט עשויה להתעכב אם הם לא מקבצי מודולים. אומנם אפשר להשתמש רמז על משאב אחד (<link rel="modulepreload">) לטעינת עצי מודול גדולים כבר בשלב מוקדם עד כמה שאפשר, עדיין עדיף להשתמש בחבילות JavaScript מאשר בביצועי הטעינה נקודת מבט.

לא להשבית בטעות הידור של סטרימינג

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

יש כמה דרכים להבטיח שהאוסף של הסטרימינג יתבצע עבור אפליקציית אינטרנט ב-Chromium:

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

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

הדגמה של ייבוא דינמי

חבילה ו-Webpack

webpack מגיע עם פלאגין בשם SplitChunksPlugin, שמאפשר לכם להגדיר את האופן שבו ה-bundler מפצל קובצי 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');
}

תצורת ברירת המחדל של חבילת האינטרנט עבור קטע הקוד הקודם מובילה לשתי אפשרויות: מקטעים נפרדים:

  • המקטע main.js (ש-webpack מסווג כמקטע initial) כולל את המודול main.js ו-./my-function.js.
  • המקטע async, שכולל רק form-validation.js (שמכיל רק גיבוב של קובץ בשם המשאב, אם הוגדר). ניתן להוריד את המקטע הזה בלבד אם וכאשר condition הוא נכון.

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

לעומת זאת, שינוי ההגדרות של SplitChunksPlugin כדי לציין הפונקציה chunks: initial מבטיחה שהקוד מפוצל רק במקטעי נתונים ראשוניים. הנושאים האלה מקטעים כמו אלה שיובאו באופן סטטי או רשומים ב-entry של ה-webpack לנכס. בהמשך לדוגמה שלמעלה, המקטע שיתקבל יהיה שילוב של form-validation.js ו- main.js בקובץ סקריפט יחיד, מה שעלול להוביל לירידה בביצועים של טעינת הדף הראשונית.

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

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

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

הדגמה של Webpack

הדגמה של SplitChunksPlugin webpack.

בוחנים את הידע

איזה סוג של הצהרה import משמש לביצוע קוד לפצל?

import() דינמי.
תשובה נכונה!
import סטטי.
אפשר לנסות שוב.

איזה סוג של משפט import חייב להופיע בחלק העליון של הדף של מודול JavaScript ולא במיקום אחר?

import() דינמי.
אפשר לנסות שוב.
import סטטי.
תשובה נכונה!

כשמשתמשים ב-SplitChunksPlugin ב-webpack, מהו את ההבדל בין מקטע async מקטע initial?

async מקטעים נטענים באמצעות import() דינמי ו-initial מקטעים נטענים באמצעות סטטיות import
תשובה נכונה!
async מקטעים נטענים באמצעות import סטטי ו-initial מקטעים נטענים באמצעות import()
אפשר לנסות שוב.

הסרטון הבא: טעינה מדורגת של תמונות ורכיבי <iframe>

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