ביצועים משופרים לטעינת דפים Next.js ו-Gatsby עם פיצול פרטני

אסטרטגיה חדשה יותר של קיבוץ חבילות אינטרנט ב-Next.js וב-Gatsby מצמצמת את הקוד הכפול כדי לשפר את הביצועים של טעינת הדפים.

Chrome משתף פעולה עם כלים בסביבה העסקית של קוד פתוח ב-JavaScript. לאחרונה בוצעו כמה אופטימיזציות חדשות יותר נוסף כדי לשפר את ביצועי הטעינה של Next.js ו- Gatsby. המאמר הזה עוסק באסטרטגיית חלוקה מפורטת יותר של הנתונים שנשלח עכשיו כברירת מחדל בשתי המסגרות.

מבוא

כמו frameworks רבות באינטרנט, Next.js וגטסבי משתמשים ב-webpack בתור הליבה שלהם. Bundler. השקה של webpack v3 CommonsChunkPlugin כדי לאפשר מודולים של פלט שמשותפים בין נקודות כניסה שונות במסגרת 'מאגר משותף' אחד (או כמה) מקטע (או) מקטעים). אפשר להוריד את הקוד המשותף בנפרד ולשמור אותו במטמון של הדפדפן בשלב מוקדם. יובילו לשיפור בביצועי הטעינה.

הדפוס הזה הפך לפופולרי ב-frameworks רבות של אפליקציות שמכילות דף יחיד, שייכנסו לתוקף תצורת חבילה שנראית כך:

הגדרה משותפת של נקודת כניסה וחבילה

למרות שמעשי לרעיון של קיבוץ כל קודי המודול המשותף במקטע יחיד יש מגבלות בפועל. אפשר להוריד מודולים שלא משותפים בכל נקודת כניסה למסלולים שלא משתמשים בה וכתוצאה מכך מתבצעת הורדה של יותר קוד מהנדרש. לדוגמה, כשpage1 נטען המקטע common, הוא טוען את הקוד עבור moduleC למרות ש-page1 לא משתמש ב-moduleC. מהסיבה הזו, יחד עם עוד כמה סיבות אחרות, הפלאגין Webpack v4 הסיר את הפלאגין לטובת אחת: SplitChunksPlugin.

חלוקה משופרת של מקטעים

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

עם זאת, מסגרות אינטרנט רבות שמשתמשות בפלאגין הזה עדיין פועלות לפי 'נחלת הכלל' הגישה למקטעי נתונים של פיצול נתונים. לדוגמה, Next.js, ייצור חבילה של commons שמכילה כל מודול משמש ביותר מ-50% מהדפים ובכל יחסי התלות של framework (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 נפרד ליחסי תלות של framework (react, react-dom ו- וכן הלאה)
  • אם צריך ליצור מקטעים משותפים רבים ככל שצריך (עד 25)
  • הגודל המינימלי של מקטע ליצירה משתנה ל-20KB

אסטרטגיית החלוקה המפורטת הזו מספקת את היתרונות הבאים:

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

אפשר לראות את כל ההגדרות האישיות שיושמו ב-Next.js ב-webpack-config.ts.

בקשות HTTP נוספות

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

דפדפנים יכולים לפתוח רק מספר מוגבל של חיבורי TCP למקור אחד (6 ב-Chrome), לכן צמצום מספר המקטעים שהפלט מופק על ידי Bundler יכול להבטיח שהמספר הכולל של הבקשות לא יעבור את הסף הזה. עם זאת, הוא נכון רק לגבי HTTP/1.1. Multiplexing ב-HTTP/2 מאפשר להזרים בקשות מרובות במקביל באמצעות חיבור יחיד המקור. במילים אחרות, בדרך כלל אין לנו צורך לחשוש לגבי הגבלת מספר המקטעים שנשלח על ידי ה-bundler שלנו.

כל הדפדפנים העיקריים תומכים ב-HTTP/2. צוותי Chrome ו-Next.js רציתי לבדוק אם להגדיל את מספר הבקשות על ידי פיצול של ערכי ה-"commons" היחידים של Next.js חבילה במספר מקטעים משותפים ישפיעו על ביצועי הטעינה בדרך כלשהי. הם התחילו במדידה של את הביצועים של אתר יחיד תוך שינוי המספר המקסימלי של בקשות מקבילות באמצעות הפונקציה maxInitialRequests לנכס.

ביצועים של טעינת דף עם מספר גבוה יותר של בקשות

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

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

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

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

הפחתת מטען ייעודי (payload) של JavaScript עם חלוקה מוגברת של נתונים במקטעים

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

הגודל המינימלי שמוגדר כברירת מחדל ליצירת מקטעים הוא 30KB. אבל, צימוד במקום זאת, הערך של maxInitialRequests של 25 בגודל מינימלי של 20KB הוביל לשמירה טובה יותר במטמון.

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

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

Next.js משתמש בקובץ מניפסט של build בצד השרת כדי לקבוע אילו מקטעי פלט ישמשו את נקודות כניסה שונות. כדי לספק את המידע הזה גם ללקוח, הסבר מקוצר בצד הלקוח נוצר קובץ מניפסט של 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.

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

אתר שינוי כולל ב-JS הבדל באחוזים
https://www.barnebys.com/ -238 KB ‎-23%
https://sumup.com/ -220 KB -30%
https://www.hashicorp.com/ -11 MB -71%
צמצום גודל JavaScript – בכל המסלולים (דחוס)

הגרסה הסופית נשלחה כברירת מחדל בגרסה 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.1 MB 35%-
https://reactjs.org/ -80 Kb ‎-8%
צמצום גודל JavaScript – בכל המסלולים (דחוס)

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

סיכום

הקונספט של מקטעי משלוח הוא לא ספציפי ל-Next.js, לגטסבי או אפילו ל-webpack. כולם לשקול לשפר את אסטרטגיית קיבוץ הנתונים של האפליקציה אם היא נובעת מ"עקרונות נפוצים" גישה לחבילה, ללא קשר ל-framework או ל-bundler של המודול שבו נעשה שימוש.

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