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

تعمل استراتيجية تقسيم webpack الأحدث في 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)[\\/]/,
      },

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