تحسين أداء تحميل الصفحات في Next.js وGatsby باستخدام ميزة التقسيم الدقيق

تساعد استراتيجية تقسيم webpack الأحدث في Next.js وGatsby على تقليل الرموز المكرّرة لتحسين أداء تحميل الصفحة.

يتعاون Chrome مع الأدوات وأُطر العمل في منظومة JavaScript المتكاملة المفتوحة المصدر. تمت مؤخرًا إضافة عدد من التحسينات الجديدة لتحسين أداء تحميل Next.js وGatsby. تتناول هذه المقالة استراتيجية محسّنة لتقسيم البيانات إلى أجزاء دقيقة يتم شحنها الآن تلقائيًا في كلا الإطارَين.

مقدمة

مثل العديد من أُطر عمل الويب، يستخدم Next.js وGatsby webpack كأداة تجميع أساسية. قدّم الإصدار الثالث من webpack CommonsChunkPlugin لإتاحة إخراج الوحدات المشتركة بين نقاط دخول مختلفة في جزء واحد (أو بضعة أجزاء) "مشتركة". يمكن تنزيل الرمز المشترَك بشكل منفصل وتخزينه في ذاكرة التخزين المؤقت للمتصفّح في وقت مبكر، ما قد يؤدي إلى تحسين أداء التحميل.

أصبح هذا النمط شائعًا مع العديد من أُطر عمل التطبيقات من صفحة واحدة التي تعتمد على نقطة دخول وإعدادات حزمة تبدو على النحو التالي:

نقطة الدخول الشائعة وإعدادات الحزمة

على الرغم من أنّ مفهوم تجميع كل الرموز البرمجية للوحدات المشتركة في حزمة واحدة هو مفهوم عملي، إلا أنّه ينطوي على بعض القيود. يمكن تنزيل الوحدات التي لا تتم مشاركتها في كل نقطة دخول للمسارات التي لا تستخدمها، ما يؤدي إلى تنزيل رمز أكثر من اللازم. على سبيل المثال، عندما يحمّل page1 الجزء common، يتم تحميل الرمز لـ moduleC على الرغم من أنّ page1 لا يستخدم moduleC. لهذا السبب، إلى جانب بعض الأسباب الأخرى، أزال الإصدار 4 من webpack المكوّن الإضافي لصالح مكوّن جديد: SplitChunksPlugin.

تقسيم محسَّن

تعمل الإعدادات التلقائية لـ SplitChunksPlugin بشكل جيد مع معظم المستخدمين. يتم إنشاء أجزاء متعددة من الرموز البرمجية المجزأة استنادًا إلى عدد من الشروط لمنع جلب الرموز البرمجية المكررة عبر مسارات متعددة.

ومع ذلك، فإنّ العديد من أُطر عمل الويب التي تستخدم هذا المكوّن الإضافي لا تزال تتّبع نهج "المشتركات الفردية" لتقسيم الأجزاء. على سبيل المثال، سينشئ 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 تقلّل من الرموز البرمجية غير الضرورية لأي مسار.

  • يتم تقسيم أي وحدة تابعة لجهة خارجية كبيرة بما يكفي (أكبر من 160 كيلوبايت) إلى أجزاء فردية خاصة بها
  • يتم إنشاء مجموعة frameworks منفصلة لتبعيات إطار العمل (react وreact-dom وما إلى ذلك)
  • يتم إنشاء أكبر عدد ممكن من الأجزاء المشتركة حسب الحاجة (ما يصل إلى 25 جزءًا)
  • تم تغيير الحد الأدنى لحجم الجزء الذي يتم إنشاؤه إلى 20 كيلوبايت

توفّر استراتيجية التقسيم الدقيق هذه المزايا التالية:

  • تحسين أوقات تحميل الصفحة يؤدي إصدار أجزاء مشتركة متعددة، بدلاً من جزء واحد، إلى تقليل كمية الرموز غير الضرورية (أو المكرّرة) لأي نقطة دخول.
  • تحسين التخزين المؤقت أثناء التنقّل: يؤدي تقسيم المكتبات الكبيرة والتبعيات الخاصة بإطار العمل إلى أجزاء منفصلة إلى تقليل احتمالية إبطال ذاكرة التخزين المؤقت، لأنّه من غير المرجّح أن يتغيّر كلاهما إلى أن تتم ترقية.

يمكنك الاطّلاع على الإعدادات الكاملة التي اعتمدها Next.js في webpack-config.ts.

طلبات HTTP إضافية

حدّد SplitChunksPlugin أساس التقسيم الدقيق، ولم يكن تطبيق هذا النهج على إطار عمل مثل Next.js مفهومًا جديدًا تمامًا. ومع ذلك، استمرت العديد من أُطر العمل في استخدام استراتيجية حِزم "المشتركة" وإرشادية واحدة لعدة أسباب. ويشمل ذلك القلق من أنّ طلبات HTTP الإضافية قد تؤثر سلبًا في أداء الموقع الإلكتروني.

يمكن للمتصفّحات فتح عدد محدود فقط من اتصالات TCP بمصدر واحد (6 لمتصفّح Chrome)، لذا فإنّ تقليل عدد الأجزاء التي ينتجها برنامج التجميع يمكن أن يضمن بقاء إجمالي عدد الطلبات أقل من هذا الحدّ. ومع ذلك، ينطبق هذا فقط على HTTP/1.1. يتيح الإرسال المتعدّد في بروتوكول HTTP/2 إمكانية بث طلبات متعددة بالتوازي باستخدام اتصال واحد عبر مصدر واحد. بعبارة أخرى، لا داعي للقلق بشأن الحدّ من عدد الأجزاء التي يصدرها برنامج التجميع.

تتوافق جميع المتصفّحات الرئيسية مع بروتوكول HTTP/2. أراد فريقا Chrome وNext.js معرفة ما إذا كانت زيادة عدد الطلبات من خلال تقسيم حزمة "commons" الواحدة في Next.js إلى أجزاء مشتركة متعددة ستؤثر في أداء التحميل بأي شكل من الأشكال. بدأوا بقياس أداء موقع إلكتروني واحد أثناء تعديل الحد الأقصى لعدد الطلبات المتزامنة باستخدام السمة maxInitialRequests.

أداء تحميل الصفحة مع زيادة عدد الطلبات

في متوسط ثلاث عمليات تشغيل لتجارب متعددة على صفحة ويب واحدة، ظلّت أوقات load وبدء العرض وسرعة عرض أول محتوى مرئي كما هي تقريبًا عند تغيير الحد الأقصى لعدد الطلبات الأولية (من 5 إلى 15). ومن المثير للاهتمام أننا لاحظنا زيادة طفيفة في الأداء فقط بعد التقسيم بقوة إلى مئات الطلبات.

أداء تحميل الصفحة مع مئات الطلبات

وأظهر ذلك أنّ البقاء تحت حدّ موثوق به (بين 20 و25 طلبًا) يحقّق التوازن الصحيح بين أداء التحميل وكفاءة التخزين المؤقت. بعد إجراء بعض الاختبارات الأساسية، تم اختيار 25 كعدد maxInitialRequest.

أدى تعديل الحد الأقصى لعدد الطلبات التي تحدث بالتوازي إلى إنشاء أكثر من حزمة مشتركة واحدة، كما أنّ فصلها بشكل مناسب لكل نقطة دخول أدى إلى تقليل كمية الرموز البرمجية غير الضرورية للصفحة نفسها بشكل كبير.

تقليل حمولة JavaScript من خلال زيادة التقسيم

كانت هذه التجربة تهدف فقط إلى تعديل عدد الطلبات لمعرفة ما إذا كان هناك أي تأثير سلبي على أداء تحميل الصفحة. تشير النتائج إلى أنّ ضبط maxInitialRequests على 25 في صفحة الاختبار كان مثاليًا لأنّه قلّل حجم حمولة JavaScript بدون إبطاء الصفحة. ظلّ إجمالي حجم JavaScript المطلوب لتحميل الصفحة كما هو تقريبًا، ما يفسّر سبب عدم تحسُّن أداء تحميل الصفحة بالضرورة مع تقليل حجم الرمز البرمجي.

يستخدم webpack‏ 30 كيلوبايت كحدّ أدنى تلقائي لحجم الجزء الذي سيتم إنشاؤه. ومع ذلك، فإنّ استخدام قيمة 25 لـ maxInitialRequests مع حدّ أدنى للحجم يبلغ 20 كيلوبايت أدى إلى تحسين التخزين المؤقت.

تقليل حجم التطبيق باستخدام أجزاء دقيقة

تعتمد العديد من إطارات العمل، بما في ذلك Next.js، على التوجيه من جهة العميل (الذي تتم معالجته بواسطة JavaScript) لإدخال علامات برمجية أحدث لكل عملية انتقال في المسار. ولكن كيف يتم تحديد هذه الأجزاء الديناميكية مسبقًا في وقت الإنشاء؟

يستخدم Next.js ملف بيان إصدار من جهة الخادم لتحديد الأجزاء الناتجة التي تستخدمها نقاط الدخول المختلفة. ولتوفير هذه المعلومات للعميل أيضًا، تم إنشاء ملف بيان إصدار مختصر من جهة العميل لربط جميع التبعيات لكل نقطة دخول.

// 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 المستخدَم في الموقع الإلكتروني بالكامل:

الموقع الإلكتروني إجمالي التغيير في JavaScript نسبة الاختلاف
https://www.barnebys.com/ ‫-238 كيلوبايت ‪-23%
https://sumup.com/ ‫-220 كيلوبايت ‎-30%
https://www.hashicorp.com/ ‫-11 ميغابايت ‎-71%
تقليل حجم JavaScript في جميع المسارات (مضغوطة)

وتم طرح الإصدار النهائي تلقائيًا في الإصدار 9.2.

Gatsby

Gatsby used to follow the same approach of using a usage-based heuristic for defining common modules:

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 في العديد من المواقع الإلكترونية الكبيرة:

الموقع الإلكتروني إجمالي التغيير في JavaScript نسبة الاختلاف
https://www.gatsbyjs.org/ ‫-680 كيلوبايت ‫-22%
https://www.thirdandgrove.com/ ‫-390 كيلوبايت ‎-25%
https://ghost.org/ ‫-1.1 ميغابايت ‎-35%
https://reactjs.org/ ‫-80 كيلوبايت -8‏%
تقليل حجم JavaScript في جميع المسارات (مضغوطة)

يمكنك الاطّلاع على طلب الدمج لفهم كيفية تنفيذ هذا المنطق في إعدادات webpack، والذي يتم شحنه تلقائيًا في الإصدار 2.20.7.

الخاتمة

لا يقتصر مفهوم شحن الأجزاء الدقيقة على Next.js أو Gatsby أو حتى webpack. يجب على الجميع التفكير في تحسين استراتيجية تقسيم التطبيق إذا كان يتّبع نهج حزمة "المشاع" الكبير، بغض النظر عن إطار العمل أو حزمة الوحدات المستخدَمة.

  • إذا أردت الاطّلاع على تحسينات التقسيم نفسها المطبَّقة على تطبيق React عادي، يمكنك إلقاء نظرة على نموذج تطبيق React هذا. يستخدم هذا النموذج نسخة مبسّطة من استراتيجية التقسيم الدقيق ويمكن أن يساعدك في البدء بتطبيق المنطق نفسه على موقعك الإلكتروني.
  • بالنسبة إلى Rollup، يتم إنشاء الأجزاء بشكل دقيق تلقائيًا. يمكنك الاطّلاع على manualChunks إذا أردت ضبط السلوك يدويًا.