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

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

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

مقدمة

مثل العديد من إطارات عمل الويب، يستخدم Next.js وGatsby webpack كأداة تجميع أساسيه. وقد أدخلت الإصدار 3 من 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 معرفة ما إذا كانت زيادة عدد الطلبات من خلال تقسيم حِزمة "العناصر الشائعة" في 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)[\\/]/,
      },

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