بهبود عملکرد بار کردن صفحه Next.js و Gatsby با تکه‌تکه کردن دانه‌ای

یک راهبرد جدیدتر تکه‌تکه کردن webpack در Next.js و Gatsby کد تکراری را برای بهبود عملکرد بارگیری صفحه به‌حداقل می‌رساند.

Chrome با ابزارها و چارچوب‌ها در بوم‌سازگان متن‌باز جاوا اسکریپت همکاری می‌کند. اخیراً تعدادی بهینه‌سازی جدیدتر برای بهبود عملکرد بار کردن Next.js و Gatsby اضافه شده است. این مقاله استراتژی بهبودیافته تکه‌تکه کردن دانه‌ای را پوشش می‌دهد که اکنون به‌طور پیش‌فرض در هر دو چارچوب ارائه می‌شود.

مقدمه

مانند بسیاری از چارچوب‌های وب، Next.js و Gatsby از webpack به‌عنوان بسته‌بندی اصلی خود استفاده می‌کنند. webpack v3 CommonsChunkPlugin را معرفی کرد تا امکان خروجی گرفتن از ماژول‌های مشترک بین نقاط ورودی مختلف در یک (یا چند) قطعه (یا قطعات) «مشترک» فراهم شود. کد مشترک را می‌توان به‌طور جداگانه بارگیری کرد و در اوایل کار در حافظه نهان مرورگر ذخیره کرد که می‌تواند منجر به عملکرد بارگیری بهتری شود.

این الگو با بسیاری از چارچوب‌های برنامه تک‌صفحه‌ای که نقطه ورود و پیکربندی بسته‌ای را اتخاذ می‌کردند، محبوب شد که به این شکل بود:

پیکربندی نقطه ورودی و بسته متداول

اگرچه مفهوم بسته‌بندی کردن همه کدهای ماژول هم‌رسانی‌شده در یک تکه واحد کاربردی است، اما محدودیت‌هایی دارد. واحدهایی که در همه نقاط ورودی هم‌رسانی نشده‌اند می‌توانند برای مسیرهایی که از آن استفاده نمی‌کنند بارگیری شوند که منجر به بارگیری کد بیشتر از حد لازم می‌شود. برای مثال، وقتی page1 تکه common را بار می‌کند، کد moduleC را بار می‌کند، هرچند page1 از moduleC استفاده نمی‌کند. به این دلیل، همراه با چند دلیل دیگر، webpack v4 این افزونه را به‌نفع افزونه جدیدی حذف کرد: SplitChunksPlugin.

تکه‌تکه کردن بهبودیافته

تنظیمات پیش‌فرض برای SplitChunksPlugin برای اکثر کاربران به‌خوبی کار می‌کند. چندین تکه تقسیم‌شده براساس تعدادی شرایط ایجاد می‌شود تا از واکشی کد تکراری در چندین مسیر جلوگیری شود.

بااین‌حال، بسیاری از چارچوب‌های وب که از این افزونه استفاده می‌کنند همچنان از رویکرد «تک‌مشترک» برای تقسیم‌بندی تکه‌ای پیروی می‌کنند. برای مثال، Next.js یک بسته commons تولید می‌کند که شامل هر واحدی است که در بیش از ۵۰٪ صفحات و همه وابستگی‌های چارچوب (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 اتخاذ کرد که کد غیرضروری را برای هر مسیر کاهش می‌دهد.

  • هر واحد شخص ثالثی که به‌اندازه کافی بزرگ باشد (بزرگ‌تر از ۱۶۰ کیلوبایت) به تکه‌های جداگانه تقسیم می‌شود
  • قطعه frameworks جداگانه‌ای برای وابستگی‌های چارچوب (react، react-dom، و غیره) ایجاد می‌شود
  • هر تعداد بخش هم‌رسانی‌شده که لازم باشد ایجاد می‌شود (تا ۲۵ بخش)
  • حداقل اندازه برای تولید یک تکه به ۲۰ کیلوبایت تغییر کرده است

این استراتژی تکه‌تکه کردن دانه‌ای مزایای زیر را ارائه می‌دهد:

  • زمان بار شدن صفحه بهبود یافته است. انتشار چند قطعه هم‌رسانی‌شده، به‌جای یک قطعه، مقدار کد غیرضروری (یا تکراری) را برای هر نقطه ورودی به‌حداقل می‌رساند.
  • بهبود ذخیره‌سازی در حافظه نهان درطول ناوبری. تقسیم کتابخانه‌های بزرگ و وابستگی‌های چارچوب به تکه‌های جداگانه، احتمال باطل شدن حافظه پنهان را کاهش می‌دهد، زیرا بعید است که هر دو تا زمان ارتقا تغییر کنند.

می‌توانید کل پیکربندی‌ای را که Next.js در webpack-config.ts اتخاذ کرده است ببینید.

درخواست‌های اچ‌تی‌تی‌پی بیشتر

SplitChunksPlugin اساس تکه‌تکه کردن دانه‌ای را تعریف کرد و اعمال این رویکرد به چارچوبی مانند Next.js مفهوم کاملاً جدیدی نبود. بااین‌حال، بسیاری از چارچوب‌ها همچنان به استفاده از یک راهبرد بسته‌ای «مشترک» و اکتشافی ادامه دادند. این شامل این نگرانی است که درخواست‌های HTTP بسیار بیشتر می‌تواند بر عملکرد سایت تأثیر منفی بگذارد.

مرورگرها فقط می‌توانند تعداد محدودی اتصال TCP به یک مبدأ واحد باز کنند (۶ اتصال برای Chrome)، بنابراین به‌حداقل رساندن تعداد تکه‌های خروجی از بسته‌بندی‌کننده می‌تواند تضمین کند که تعداد کل درخواست‌ها زیر این آستانه باقی می‌ماند. اما این فقط برای HTTP/1.1 صادق است. هم‌تافتگری در HTTP/2 امکان جاری‌سازی موازی چندین درخواست را بااستفاده از یک اتصال در یک مبدأ فراهم می‌کند. به عبارت دیگر، ما معمولاً نیازی به نگرانی درباره محدود کردن تعداد تکه‌های منتشرشده توسط بسته‌بندی‌کننده خود نداریم.

همه مرورگرهای اصلی از HTTP/2 پشتیبانی می‌کنند. تیم‌های Chrome و Next.js می‌خواستند ببینند آیا افزایش تعداد درخواست‌ها با تقسیم کردن بسته تکی «مشترکات» Next.js به چندین بخش هم‌رسانی‌شده تأثیری بر عملکرد بار کردن دارد یا نه. آن‌ها با اندازه‌گیری عملکرد یک سایت واحد شروع کردند و درعین‌حال حداکثر تعداد درخواست‌های موازی را بااستفاده از ویژگی maxInitialRequests اصلاح کردند.

عملکرد بار کردن صفحه با افزایش تعداد درخواست‌ها

در میانگین سه اجرای چند آزمایشی در یک صفحه وب، load، شروع پرداز و زمان‌های اولین پرداز محتوا همگی هنگام تغییر حداکثر تعداد درخواست اولیه (از ۵ به ۱۵) تقریباً یکسان باقی ماندند. جالب است که فقط پس‌از تقسیم تهاجمی به صدها درخواست، متوجه سربار عملکردی جزئی شدیم.

عملکرد بار کردن صفحه با صدها درخواست

این نشان داد که ماندن زیر یک آستانه قابل‌اعتماد (۲۰ تا ۲۵ درخواست) تعادل مناسبی بین عملکرد بارگیری و کارایی ذخیره‌سازی موقت ایجاد می‌کند. پس‌از چند آزمایش خط مبنا، ۲۵ به‌عنوان تعداد maxInitialRequest انتخاب شد.

اصلاح حداکثر تعداد درخواست‌هایی که به‌طور موازی اتفاق می‌افتند منجر به بیش‌از یک بسته مشترک شد و جدا کردن مناسب آن‌ها برای هر نقطه ورودی، مقدار کد غیرضروری را برای همان صفحه به‌طور قابل‌توجهی کاهش داد.

کاهش بار داده جاوا اسکریپت با افزایش تکه‌تکه کردن

این آزمایش فقط درباره اصلاح تعداد درخواست‌ها بود تا ببینیم آیا تأثیر منفی بر عملکرد بار کردن صفحه دارد یا نه. نتایج نشان می‌دهد که تنظیم maxInitialRequests روی 25 در صفحه آزمایشی بهینه بود زیرا اندازه بار داده جاوا اسکریپت را بدون کند کردن صفحه کاهش داد. مقدار کل جاوا اسکریپت موردنیاز برای هیدراته کردن صفحه همچنان تقریباً یکسان باقی ماند، که توضیح می‌دهد چرا عملکرد بارگیری صفحه لزوماً با کاهش مقدار کد بهبود نیافته است.

webpack از ۳۰ کیلوبایت به‌عنوان حداقل اندازه پیش‌فرض برای تولید تکه استفاده می‌کند. اما، جفت کردن maxInitialRequests مقدار ۲۵ با حداقل اندازه ۲۰ کیلوبایت منجر به ذخیره‌سازی بهتر در حافظه نهان شد.

کاهش اندازه با تکه‌های دانه‌ای

بسیاری از چارچوب‌ها، ازجمله Next.js، به مسیریابی سمت کارخواه (که توسط جاوااسکریپت مدیریت می‌شود) متکی هستند تا برچسب‌های دستورگان جدیدتری را برای هر انتقال مسیر تزریق کنند. اما چگونه این تکه‌های پویا را در زمان ساخت ازپیش تعیین می‌کنند؟

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 پشت پرچم عرضه شد و در آنجا روی تعدادی از پذیرندگان آغازین آزمایش شد. بسیاری از آن‌ها شاهد کاهش قابل‌توجهی در کل جاوا اسکریپت استفاده‌شده برای کل سایت خود بودند:

وب‌سایت مجموع تغییرات جاوااسکریپت ٪ تفاوت
https://www.barnebys.com/ ‫-۲۳۸ کیلوبایت ‪-۲۳٪
https://sumup.com/ -۲۲۰ کیلوبایت ‫-٪۳۰
https://www.hashicorp.com/ ‫-۱۱ مگابایت ‫-۷۱٪
کاهش اندازه جاوا اسکریپت - در همه مسیرها (فشرده‌شده)

نسخه نهایی به‌طور پیش‌فرض در نسخه ۹.۲ عرضه شد.

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 خود برای اتخاذ یک استراتژی مشابه تکه‌تکه کردن دانه‌ای، آن‌ها همچنین متوجه کاهش قابل توجه جاوا اسکریپت در بسیاری از سایت‌های بزرگ شدند:

وب‌سایت مجموع تغییرات جاوااسکریپت ٪ تفاوت
https://www.gatsbyjs.org/ -۶۸۰ کیلوبایت ‫-۲۲٪
https://www.thirdandgrove.com/ ‫-۳۹۰ کیلوبایت ‫-٪۲۵
https://ghost.org/ ‫-۱٫۱ مگابایت ‫-۳۵٪
https://reactjs.org/ -۸۰ کیلوبایت ‫-۸٪
کاهش اندازه جاوا اسکریپت - در همه مسیرها (فشرده‌شده)

به درخواست ارسال نگاهی بیندازید تا متوجه شوید آن‌ها چگونه این منطق را در پیکربندی webpack خود پیاده‌سازی کرده‌اند که به‌طور پیش‌فرض در نسخه ۲.۲۰.۷ ارائه می‌شود.

نتیجه‌گیری

مفهوم ارسال تکه‌های دانه‌ای مختص Next.js،‏ Gatsby، یا حتی webpack نیست. همه باید بهبود استراتژی تکه‌تکه کردن برنامه خود را درنظر بگیرند اگر از رویکرد بسته بزرگ «مشترک» پیروی می‌کند، صرف‌نظر از چارچوب یا بسته‌بندی ماژول استفاده‌شده.

  • اگر می‌خواهید ببینید که چگونه همان بهینه‌سازی‌های تکه‌تکه کردن بر یک برنامه React ساده اعمال می‌شود، به این نمونه برنامه React نگاهی بیندازید. این برنامه از یک نسخه ساده‌شده از استراتژی تکه‌تکه کردن دانه‌ای استفاده می‌کند و می‌تواند به شما کمک کند تا همان نوع منطق را در سایت خود اعمال کنید.
  • برای Rollup، تکه‌ها به‌طور پیش‌فرض به‌صورت دانه‌ای ایجاد می‌شوند. اگر می‌خواهید رفتار را به‌صورت دستی پیکربندی کنید، manualChunks را ببینید.