אסטרטגיית חלוקה חדשה יותר של webpack ב-Next.js וב-Gatsby מצמצמת את כמות הקוד הכפול כדי לשפר את ביצועי טעינת הדף.
אנחנו ב-Chrome משתפים פעולה עם כלים וסביבות עבודה בסביבה העסקית של קוד פתוח ב-JavaScript. לאחרונה נוספו כמה אופטימיזציות חדשות כדי לשפר את ביצועי הטעינה של Next.js ו-Gatsby. במאמר הזה נסביר על אסטרטגיית חלוקה משופרת ומפורטת יותר, שמוגדרת עכשיו כברירת מחדל בשתי המסגרות.
מבוא
כמו הרבה סביבות פיתוח לאינטרנט, Next.js ו-Gatsby משתמשים ב-webpack ככלי הליבה שלהם לאיסוף חבילות. בגרסה 3 של webpack נוסף CommonsChunkPlugin כדי לאפשר פלט של מודולים שמשותפים בין נקודות כניסה שונות בחלק (או בחלקים) "משותפים" בודדים (או מעטים). אפשר להוריד קוד משותף בנפרד ולשמור אותו במטמון הדפדפן בשלב מוקדם, וכך לשפר את ביצועי הטעינה.
התבנית הזו הפכה לפופולרית, והרבה מסגרות של אפליקציות דף יחיד אימצו הגדרה של נקודת כניסה וחבילה שנראית כך:

למרות שזה מעשי, לרעיון של קיבוץ כל הקוד של המודולים המשותפים לחלק אחד יש מגבלות. מודולים שלא משותפים בכל נקודת כניסה יכולים לרדת במסלולים שלא משתמשים בהם, וכתוצאה מכך יורד יותר קוד מהנדרש. לדוגמה, כש-page1 טוען את החלק common, הוא טוען את הקוד של moduleC, למרות ש-page1 לא משתמש ב-moduleC.
מהסיבה הזו, ומכמה סיבות נוספות, ב-webpack v4 הפלאגין הוסר והוחלף בפלאגין חדש: SplitChunksPlugin.
חלוקה משופרת לחלקים
הגדרות ברירת המחדל של SplitChunksPlugin מתאימות לרוב המשתמשים. נוצרים כמה קטעים מפוצלים בהתאם למספר תנאים כדי למנוע אחזור של קוד כפול בכמה נתיבים.
עם זאת, הרבה מסגרות אינטרנט שמשתמשות בפלאגין הזה עדיין פועלות לפי גישת single-commons לפיצול מקטעים. לדוגמה, Next.js ייצור חבילת commons שתכיל כל מודול שנעשה בו שימוש ביותר מ-50% מהדפים וכל יחסי התלות של המסגרת (react, react-dom וכו').
const splitChunksConfigs = {
…
prod: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
},
react: {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
},
},
},
הכללת קוד תלוי-מסגרת בנתח משותף מאפשרת להוריד אותו ולשמור אותו במטמון לכל נקודת כניסה, אבל ההיוריסטיקה שמבוססת על שימוש בהכללת מודולים נפוצים שנעשה בהם שימוש ביותר מחצי מהדפים לא יעילה במיוחד. אם נשנה את היחס הזה, תהיה לכך אחת משתי תוצאות:
- אם מקטינים את היחס, המערכת מורידה יותר קוד מיותר.
- אם מגדילים את היחס, יותר קוד משוכפל בכמה נתיבים.
כדי לפתור את הבעיה, ב-Next.js משתמשים בהגדרה אחרת של SplitChunksPlugin שמפחיתה קוד מיותר בכל נתיב.
- כל מודול של צד שלישי שהוא גדול מספיק (יותר מ-160KB) מפוצל לחלק נפרד משלו.
- נוצר מקטע
frameworksנפרד ליחסי תלות של מסגרת (react,react-domוכו') - המערכת יוצרת כמה קטעים משותפים שצריך (עד 25)
- הגודל המינימלי ליצירת קטע השתנה ל-20KB
אסטרטגיית החלוקה המפורטת הזו מספקת את היתרונות הבאים:
- זמני טעינה של דפים משתפרים. אם פולטים כמה קטעי קוד משותפים במקום קטע קוד אחד, אפשר לצמצם את כמות הקוד המיותר (או הכפול) בכל נקודת כניסה.
- שמירה משופרת במטמון במהלך הניווט. פיצול של ספריות גדולות ותלויות-מסגרת לחלקים נפרדים מקטין את הסיכוי לביטול תוקף המטמון, כי סביר להניח ששניהם לא ישתנו עד לשדרוג.
אפשר לראות את כל ההגדרות ש-Next.js אימצה ב-webpack-config.ts.
עוד בקשות HTTP
SplitChunksPlugin הגדיר את הבסיס לחלוקה מפורטת, והחלת הגישה הזו על מסגרת כמו Next.js לא הייתה קונספט חדש לגמרי. עם זאת, בהרבה ספריות עדיין משתמשים באסטרטגיה של חבילה אחת עם היוריסטיקה ו-commons, מכמה סיבות. זה כולל את החשש שמספר גדול יותר של בקשות HTTP עלול להשפיע לרעה על ביצועי האתר.
דפדפנים יכולים לפתוח רק מספר מוגבל של חיבורי TCP למקור אחד (6 ב-Chrome), כך שצמצום מספר החלקים שנוצרים על ידי כלי לאריזת קוד יכול להבטיח שהמספר הכולל של הבקשות יישאר מתחת לסף הזה. אבל זה נכון רק ל-HTTP/1.1. ריבוב ב-HTTP/2 מאפשר להעביר כמה בקשות במקביל באמצעות חיבור יחיד דרך מקור אחד. במילים אחרות, בדרך כלל אנחנו לא צריכים לדאוג לגבי הגבלת מספר הנתחים שנוצרים על ידי ה-bundler שלנו.
כל הדפדפנים המובילים תומכים ב-HTTP/2. הצוותים של Chrome ו-Next.js רצו לבדוק אם הגדלת מספר הבקשות על ידי פיצול החבילה המשותפת היחידה של Next.js לכמה חלקים משותפים תשפיע על ביצועי הטעינה בדרך כלשהי. הם התחילו במדידת הביצועים של אתר אחד, תוך שינוי המספר המקסימלי של בקשות מקבילות באמצעות המאפיין maxInitialRequests.

בממוצע של שלוש הרצות של כמה ניסיונות בדף אינטרנט אחד, הזמנים של load, start-render והצגת תוכן ראשוני (FCP) נשארו פחות או יותר זהים כששינינו את מספר הבקשות הראשוניות המקסימלי (מ-5 ל-15). באופן מעניין, הבחנו בתקורה קלה בביצועים רק אחרי פיצול אגרסיבי למאות בקשות.

הבדיקה הראתה ששמירה על סף אמין (20-25 בקשות) יצרה את האיזון הנכון בין ביצועי הטעינה לבין יעילות השמירה במטמון. אחרי כמה בדיקות בסיסיות, נבחר המספר 25 בתור הספירה של maxInitialRequest.
שינוי המספר המקסימלי של בקשות שמתרחשות במקביל הוביל ליותר מחבילה משותפת אחת, והפרדה שלהן באופן שמתאים לכל נקודת כניסה הפחיתה באופן משמעותי את כמות הקוד המיותר לאותו דף.

הניסוי הזה נועד רק לשנות את מספר הבקשות כדי לבדוק אם תהיה השפעה שלילית על ביצועי טעינת הדף. התוצאות מצביעות על כך שהגדרת maxInitialRequests לערך 25 בדף הבדיקה הייתה אופטימלית, כי היא הקטינה את גודל המטען הייעודי (payload) של JavaScript בלי להאט את הדף. כמות ה-JavaScript הכוללת שנדרשה כדי להפעיל את הדף נשארה פחות או יותר אותו דבר, וזה מסביר למה ביצועי טעינת הדף לא בהכרח השתפרו עם הפחתת כמות הקוד.
כברירת מחדל, webpack משתמש ב-30KB כגודל המינימלי ליצירת מקטע. עם זאת, שילוב של ערך maxInitialRequests של 25 עם גודל מינימלי של 20KB הוביל לשמירה טובה יותר במטמון.
הקטנת הגודל באמצעות חלוקה לחלקים קטנים יותר
הרבה סביבות פיתוח, כולל Next.js, מסתמכות על ניתוב בצד הלקוח (שמטופל על ידי JavaScript) כדי להוסיף תגי סקריפט חדשים יותר לכל מעבר בין נתיבים. אבל איך הם קובעים מראש את הנתחים הדינמיים האלה בזמן ה-build?
Next.js משתמש בקובץ מניפסט של גרסה בצד השרת כדי לקבוע באילו מקטעי פלט משתמשות נקודות כניסה שונות. כדי לספק את המידע הזה גם ללקוח, נוצר קובץ מניפסט מקוצר של גרסת build בצד הלקוח, שממפה את כל יחסי התלות של כל נקודת כניסה.
// Returns a promise for the dependencies for a particular route
getDependencies (route) {
return this.promisedBuildManifest.then(
man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
)
}
אסטרטגיית החלוקה החדשה והמפורטת יותר הושקה לראשונה ב-Next.js מאחורי דגל, שם היא נבדקה על מספר מראשוני המשתמשים. רבים מהם ראו הפחתה משמעותית בכמות הכוללת של קוד JavaScript שנעשה בו שימוש באתר כולו:
| אתר | שינוי כולל ב-JS | הבדל באחוזים |
|---|---|---|
| https://www.barnebys.com/ | -238 KB | -23% |
| https://sumup.com/ | -220 KB | -30% |
| https://www.hashicorp.com/ | -11MB | -71% |
הגרסה הסופית נשלחה כברירת מחדל בגרסה 9.2.
Gatsby
בעבר, ב-Gatsby השתמשו באותה גישה של שימוש ביוריסטיקה מבוססת-שימוש כדי להגדיר מודולים נפוצים:
config.optimization = {
…
splitChunks: {
name: false,
chunks: `all`,
cacheGroups: {
default: false,
vendors: false,
commons: {
name: `commons`,
chunks: `all`,
// if a chunk is used more than half the components count,
// we can assume it's pretty global
minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
},
react: {
name: `commons`,
chunks: `all`,
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
הם ביצעו אופטימיזציה של הגדרות ה-webpack שלהם כדי לאמץ אסטרטגיית חלוקה דומה, וגם הם הבחינו בהפחתה משמעותית של JavaScript באתרים גדולים רבים:
| אתר | שינוי כולל ב-JS | הבדל באחוזים |
|---|---|---|
| https://www.gatsbyjs.org/ | -680 KB | -22% |
| https://www.thirdandgrove.com/ | -390 KB | -25% |
| https://ghost.org/ | -1.1MB | -35% |
| https://reactjs.org/ | 80Kb- | -8% |
כדאי לעיין בבקשת ה-Pull כדי להבין איך הם הטמיעו את הלוגיקה הזו בהגדרות של webpack, שנשלחות כברירת מחדל בגרסה 2.20.7.
סיכום
הרעיון של שליחת מקטעים מפורטים לא ייחודי ל-Next.js, ל-Gatsby או אפילו ל-webpack. אם אתם משתמשים בגישה של חבילת commons גדולה, כדאי לכם לשקול לשפר את אסטרטגיית החלוקה של האפליקציה, בלי קשר למסגרת או למאגד המודולים שבהם אתם משתמשים.
- אם אתם רוצים לראות את אותן אופטימיזציות של חלוקה לקטעים שחלות על אפליקציית React רגילה, עיינו באפליקציית React לדוגמה. היא משתמשת בגרסה פשוטה של אסטרטגיית החלוקה המפורטת לקטעים, ויכולה לעזור לכם להתחיל להחיל את אותו סוג של לוגיקה על האתר שלכם.
- ב-Rollup, מקטעים נוצרים באופן פרטני כברירת מחדל. אם אתם רוצים להגדיר את ההתנהגות באופן ידני, עיינו במאמר
manualChunks.