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

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

Houssein Djirdeh
Houssein Djirdeh

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

مقدمة

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

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

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

على الرغم من أنّ تجميع كل رموز الوحدات المشترَكة في جزء واحد هو أمر عملي، إلا أنّ هذا المفهوم له بعض القيود. يمكن تنزيل الوحدات التي لا تتم مشاركتها في كل نقطة دخول للطرق التي لا تستخدمها، ما يؤدي إلى تنزيل المزيد من الرموز أكثر من اللازم. على سبيل المثال، عند تحميل الجزء page1، يتم تحميل الرمز الخاص بـ common، حتى إذا لم يستخدم page1 الجزء common.moduleCmoduleC لهذا السبب، بالإضافة إلى بعض الأسباب الأخرى، أزالت الإصدار 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 معرفة ما إذا كان تقسيم حزمة "العناصر المشتركة" الفردية في Next.js إل�� ��د�� ��جزاء مشتركة سيؤثر في أداء التحميل بأي شكل من الأشكال. بدأوا بقياس أداء موقع إلكتروني واحد مع تعديل الحد الأقصى لعدد الطلبات المتوازية باستخدام السمة maxInitialRequests.

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

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

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

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

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

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

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

تستخدم أداة webpack حجم 30 كيلوبايت كحدّ أدنى تلقائي لإنشاء مقطع. ومع ذلك، أدى ربط قيمة maxInitialRequests تبلغ 25 بحجم أدنى يبلغ 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 تتّبع النهج نفسه في استخدام إرشادات مستندة إلى الاستخدام لتحديد الوحدات الشائعة:

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 إذا أردت ضبط السلوك يدويًا.