چطور بهترین پرفورمنس ممکن رو از یه وبسایت بگیریم؟

اخیرا یه پروژه فریلنسری داشتم که پرفورمنس براش خیلی مهم بود. یه وبلاگ ساده بود که کارفرما از امتیازی که توی Lighthouse میگرفت راضی نبود و میخواست تو موبایل امتیاز بالا ۹۰ بگیره. سایت قبلیشون که با وردپرس ساخته شده بود امتیاز ۸۰ میگرفت. گویا این امتیاز تو سئو تاثیر داره. من چون به این مباحث علاقه داشتم تونستم با یکم حرفای قلمبه سلمبه مخ طرف رو بزنم و پروژه رو بگیرم. البته بعدا به مشکل خوردیم و پروژه به کس دیگه ای واگذار شد ولی این قطعه هنری برام باقی موند که امتیاز ۹۹ میگیره (با یکم تغییر میتونم به ۱۰۰ مطلق هم برسونمش):

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

میدونم مقایسه یه سایت نیمه کاره با یه سایت کامل که کلی خرت و پرت داره عادلانه نیست ولی خب تفاوت امتیاز هم قابل توجهه.

اینجا یکم در مورد تجربیاتی که توی این پروژه به دست آوردم و چیز هایی که شما باید بهش توجه کنید صحبت میکنم.

امتیاز Lighthouse به خیلی چیز ها بستگی داره ولی من فقط در مورد بخش جاوااسکریپت کار حرف میزنم که هم برای من و احتمالا برای شما جذاب تره. در ضمن من اینجا فقط به پرفورمنس کار دارم و به بقیه مسائل مثل سئو نمیپردازم. یکسری توصیه های عمومی مثل "حجم جاوااسکریپت رو کم کنید" هم توی این مقاله جا نداره. اینجا میخوایم یکم توی مبحث عمیق بشیم!

تعاریف

  • Lighthouse: یه ابزار که توسط گوگل برای بررسی کیفیت وبسایت ها ساخته شده. من معمولا با سایت pagespeed.web.dev پرفورمنس سایت هارو بررسی میکنم که نمیدونم از Lighthouse استفاده میکنه یا نه ولی امکانات مشابهی داره.
  • رندر شدن: در این مقاله رندن شدن دو معنی میتونه داشته باشه. توی سرور به معنی تولید HTML هست و توی مرورگر به معنی منظور نمایش اون چیز توی مرورگر هست.

چنتا از معیار های Lighthouse که اینجا باهاش سر و کار داریم:

First Contentful Paint (FCP): مدت زمانی که طول میکشه تا یه چیزی تو صفحه رندر بشه

Largest Contentful Paint (LCP): مدت زمانی که طول میکشه تا بزرگترین عکس یا متن توی صفحه رندر بشه

Time to Interactive (TTI): مدت زمانی که طول میکشه تا صفحه کاملا interactive بشه. این یعنی سایت باید به کنش های کاربر واکنش مناسب نشون بده، event handler ها به المنت های مربوطه وصل شده باشه و و در کلام سایت اونطور که باید، کار کنه.

شیوه های رندر شدن یه وبسایت

اینکه وبسایت شما کی و کجا رندر بشه تاثیر زیادی روی پرفورمنس و سرعت سایت شما داره. ۲ روش کلی برای این کار وجود داره:

رندر شدن سمت کاربر یا Client-side Rendering (CSR)

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

معایب

  • قبل از اینکه هر چیزی بتونه توی صفحه نشون داده بشه، باید یه فایل جاواااسکریپت عظیم دانلود و اجرا بشه. این به این معنیه که کاربر محتوا سایت رو دیرتر میبینه. این مستقیما توی FCP تاثیر میگذاره. اگر یه سایت محتوا محور مثل ویرگول یا یه فروشگاه دارید این روش زیاد مناسب شما نیست.

مزایا

  • از نظر پرفورمنس من مزیت خاصی توی این روش نمیبینم. ولی اگر یه داشبورد خیلی پیچیده یا سایتی مثل فیگما دارید و در کل سایت شما محتوا محور نیست بهتره از این روش استفاده بشه تا از هزینه های Hydration جلوگیری بشه که بعدا بهش میپردازم.

رندر شدن سمت سرور یا Server-side Rendering (SSR)

در این روش سایت ۲ بار رندر میشه! یک بار توی سرور و یک بار توی مرورگر. وقتی یه سایت که سمت سرور رندر میشه رو باز میکنید، برعکس CSR، یه فایل HTML کامل میگیرید که همه محتوا صفحه رو در بر داره. در نتیجه مرورگر خیلی سریع اون HTML رو نمایش میده و کاربر محتوا سایت رو زودتر میبینه چون نیازی به دانلود و اجرا جاوااسکریپت نیست. در این مرحله فقط یه فایل HTML داریم و سایت interactive نیست و نمیشه باهاش کار کرد. اینجاست که دوباره همون جاوااسکریپت ها دانلود و اجرا میشه تا سایت Interactive بشه. به این مرحله Hydration (هایدریشن) میگن. میدونم یکم گیج کنندس. حتما پیشنهاد میکنم که مقاله قبلیم در مورد Hydration رو بخونید که این مبحث بیشتر براتون جا بیفته.

https://virgool.io/@ahhshm/%D9%86%DA%AF%D8%A7%D9%87%DB%8C-%D8%A8%D9%87-%D9%81%D8%B1%D8%A7%DB%8C%D9%86%D8%AF-hydration-%D8%AF%D8%B1-%D9%81%D8%B1%DB%8C%D9%85%D9%88%D8%B1%DA%A9-%D9%87%D8%A7%DB%8C-%D8%AC%D8%A7%D9%88%D8%A7%D8%A7%D8%B3%DA%A9%D8%B1%DB%8C%D9%BE%D8%AA-rokrm9czwvq1

معایب

  • مرحله Hydration بزرگترین مشکل SSR هست. تو این روش، محتوا اصلی سایت زودتر نمایش داده میشه ولی سایت تا وقتی Hydration کامل نشه قابل استفاده نیست و طولانی شدن Hydration میتونه تاثیر بدی روی تجربه کاربران بگذاره چون یه چیزی تو سایت میبینند ولی نمیتونن باهاش کار کنند. به علاوه، Hydration یعنی جاوااسکریپت بیشتر چون یکسری کار های اضافه توی این مرحله باید انجام بشه. این ها به کنار، Hydration کلا دوباره کاری هست چون همونطور که گفتم سایت ۲ بار رندر میشه. پیشنهاد میکنم مقاله بالا رو بخونید تا بیشتر متوجه مشکلاتش بشید. ولی نگران نباشید، راهکار هایی وجود داره که بعدا بهش میپردازم.

مزایا

  • برای سایت های محتوا محور مثل وبلاگ و فروشگاه این روش گزینه بهتری نسبت به CSR هست چون به هر حال محتوا اولویت اول شماست.
  • توی SSR، یکسری کار ها میتونه توی سرور انجام بشه، مثل data fetching و این یه مزیت هست چون توی سرور به دلایل مختلف سریعتر از کاربران به دیتابیس و api ها دسترسی دارید. در نتیجه میتونید مقداری از بار data fetching رو از دوش کاربر ها بردارید.

خلاصه

بحث های بالا یکم پراکنده و گیج کننده هست. این خلاصه ای از تاثیر هرکدوم از روش ها روی امتیاز های Lighthouse هست:

رندر شدن سمت کاربر (CSR): First Contentful Paint و Largest Contentful Paint نسبتا بالا هست چون قبل از اینکه چیزی توی صفحه رندر بشه نیاز به دانلود و اجرا یه فایل جاوااسکریپت بزرگ هست.

رندر شدن سمت سرور(SSR): First Contentful Paint و Largest Contentful Paint خیلی پایین هست چون HTML کامل صفحه رو همون اول میگیریم و مرورگر خیلی سریع میتونه اون رو نمایش بده.

هر دو روش Time to Interactive نسبتا یکسانی دارند ولی تعجب نمیکنم اگر TTI توی SSR بیشتر از CSR طول بکشه (به خاطر Hydration).

Server-side Generation (SSG) یا Pre-rendering

این رو نتونستم توی بخش بالا قرار بدم چون به نظرم زیر مجموعه SSR هست. تو این روش، وبسایت سمت سرور رندر میشه ولی اینکار به جای اینکه تو هر ریکوئست انجام بشه فقط یکبار انجام میشه. فرض کنید یه وبلاگ دارید که خیلی محتواش تغییر نمیکنه. توی همچین سایتی واقعا نیازی نیست با هر ریکوئست سایت توی سرور رندر بشه. میتونید خیلی راحت، بعد از هر بار تغییر، سایت رو یک بار رندر کنید و خروجی رو به CDN بدید تا با سرعت بالایی اون رو به کاربران برسونه. واضحه که این روش برای سایت هایی که پویا تر هستند و هر لحظه ممکنه تغییر کنند مناسب نیست چون pre-render کردن یه سایت زمان میبره.

این روش بازم نیاز به Hydration داره. در واقع تنها مزیتش نسبت به SSR اینه که صفحات سایت فقط یک بار رندر میشن و این اتفاق توی هر ریکوئست نمیفته در نتیجه میتوه FCP و LCP رو کم کنه.

حل مشکل Hydration

همونطور که گفتم Hydration بزرگترین مشکل SSR و SSG هست و از هر زاویه که بهش نگاه کنید این مرحله اضافی و دوباره کاری هست. اگر بتونید هزینه های این مرحله رو کاهش بدید یا حتی حذف کنید پرفورمنس بهتری میگیرید. خبر خوب اینه که این کار توسط ۲ فریمورک فوق العاده داره انجام میشه:‌ Astro و Qwik.

فریمورک Astro از تکنیکی به اسم Partial Hydration استفاده میکنه. به طور خیلی خلاصه، تو این روش هر بخش و کامپوننت از سایت شما میتونه به صورت مستقل Hydrate بشه یا به بیان دیگه جاوااسکریپت مربوط بهش دانلود و اجرا بشه. مثلا فرض کنید یه کامپوننت دارید که آخر صفحه هست. کاربر ممکنه اصلا به آخر صفحه اسکرول نکنه و در نتیجه به اون کامپوننت هم نیازی نشه، خب چرا اون کامپوننت رو Hydrate کنیم؟ Partial Hydration مثلا lazy loading میمونه برای Hydration. این روش درمان قطعی برای مشکلات Hydration نیست ولی میتونه هزینه هاش رو کم کنه.

فریمورک Qwik از تکنیکی به اسم Resumability استفاده میکنه. این تکنیک نیاز به Hydration رو کاملا از بین میبره. اصلا چرا به Hydration نیاز داریم؟ وقتی یه صفحه سمت سرور رندر میشه، سرور تمام کار هایی که انجام شده رو دور میریزه و فقط HTML نهایی رو برمیگردونه پس مجبوریم صفحه رو دوباره سمت کاربر رندر کنیم. Qwik کار هایی که تو سرور انجام شده رو دور نمیریزه و اون هارو توی خود HTML نهایی جا سازی میکنه. به این ترتیب وبسایت به محض اینکه رندر شد Interactive هست و نیازی به Hydration نداره. در ضمن از کامپوننت های React هم پشتیبانی میکنه و میتونید از کتابخونه هایی که برای React ساخته شده استفاده کنید.

من در آینده نزدیک یه مقاله مفصل در مورد این دو تکنیک خواهم نوشت و اگر به این مباحث علاقه دارید حتما من رو دنبال کنید.

خب کی از Astro و Qwik استفاده کنیم؟ فریمورک Qwik خیلی جدیده و تازه چند روز پیش نسخه بتا رو منتشر کرد، در نتیجه پیشنهاد نمیشه فعلا توی پروژه های بزرگ ازش استفاده کنید ولی حتما یه نگاه بهش بندازید و ازش حمایت کنید. ایده های خیلی جالبی داره و قطعا آینده درخشانی داره. در مقابلش Astro خیلی وقته ساخته شده و الان خیلی پایدار هست. اگر یه وبلاگ یا فروشگاه اینترنتی یا همچین سایتی دارید که پرفورمنس براش مهمه Astro یکی از بهترین گزینه هاست. یه چیز جالب در مورد Astro اینه که به هیچ فریمورکی برای ساخت کامپوننت وابسته نیست. علاوه بر سیستم کامپوننتی که خودش داره، از React.js، Svelte، Vue، Solid.js و Preact پشتیبانی میکنه!!! عملا میتونید توی یه صفحه یه کامپوننت React داشته باشید، یه Vue، یه Svelte و یه Solid و هر کدوم رو به صورت مستقل کنترل کنید! جالبه نه؟

کاری که من کردم

میرسیم به اینکه من چیکار کردم. وقتی کارفرما از اهمیت پرفورمنس گفت من ۴ گزینه جلوش گذاشتم:

  1. Next.js
  2. Astro + React
  3. Astro + Solid.js
  4. Qwik

با هر کدوم یه نمونه اولیه ساده ساختم که پرفورمنس رو مقایسه کنیم. قرار بود سایت بعدا گسترش داده بشه و یه فروشگاه اینترنتی بهش اضافه بشه و با توجه به وسواس شدید کارفرما روی نمره ۱۰۰ کلا ما React رو کنار گذاشتیم. React به خودی خود حدود 44KB جاوااسکریپت داره و این مقدار وقتی چنتا کتابخونه اضافه میشه به شدت زیاد میشه و این پرفورمنس رو تحت تاثیر قرار میداد. Qwik هم گزینه خیلی خوبی بود ولی چون اون موقع هنوز خیلی جدید بود و مشکلاتی داشت نتونستیم ازش استفاده کنیم. میموند Astro + Solid.js. اگر با Solid آشنا نیستید، یه کتابخونه خیلی سبک و سریع هست که سینتکسی شبیه React داره. من خیلی ازش خوشم اومد، مخصوصا با مفهوم Reactivity که احتمالا یه مقاله در موردش بنویسم خیلی حال کردم. Solid واقعا سریعه. هم توی سرور هم توی کلاینت و فقط 7KB حجم داره! ترکیبش با Astro فوق العاده میشه. سالید سریع و سبکه و Astro هم Partial Hydration داره. در نهایت هم این وبلاگ با Solid و Astro ساخته شد که راز پرفورمنس خوبش هست. قرار بود وبسایت رو pre-render کنیم و خروجی رو به یه CDN بدیم ولی دیگه نشد که تا اون مراحل جلو برم. انتظار میره پرفورمنس سایت با گسترده شدنش افت زیادی نکنه و این رو مدیون Solid و Astro هستیم. فریمورک های خوب دیگه ای مثل Svelte هم بود ولی ترکیب Astro و Solid اینقدر خوب شد که دیگه به اون نگاهی ننداختیم.

البته حالا که اینقدر ازش تعریف کردم یکم از بدی های Solid هم بگم. کامیونیتی کوچیکش بزرگترین و شاید تنها مشکلش باشه. به سختی میتونید کتابخونه هایی پیدا کنید که برای Solid نوشته شده باشه و بعضی وقتا مجبورید خودتون یه چیزی بسازید که زیاد ایده آل نیست. البته به خاطر ساختار Solid خیلی راحت میتونید از کتابخونه هایی که با جاوااسکریپت نوشته شدند استفاده کنید. خوشبختانه این روز ها Solid به عنوان یکی از فریمورک های اصلی دیده میشه. مثلا TanStack که شامل تعدادی از بهترین کتابخونه های React هست (react-query, react-table, react-location, react-virtual) به تازگی مستقل از فریمورک شده و همه قراره یه نسخه Solid هم داشته باشند که خبر خوبیه.