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

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

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

مقدمة

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

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

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

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

تحسين التقسيم

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

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

تستخدم حزمة الويب 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.

غاتسبي

اعتاد غاتسبي اتّباع النهج نفسه المتّبع في استخدام إشارات إرشادية قائمة على الاستخدام لتحديد الوحدات الشائعة:

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 - في جميع المسارات (مضغوط)

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

الخلاصة

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

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