ویرگول
ورودثبت نام
حسان امینی لو
حسان امینی لوبرنامه نویس از جلو
حسان امینی لو
حسان امینی لو
خواندن ۱۰ دقیقه·۳ سال پیش

چند نکته برای بهبود Performance در React

به عنوان کسی که تجربه کار روی پروژه های کوچک و بزرگ و خیلی بزرگ رو با ری-اکت داشتم، به نظرم رسید که نکته هایی رو باهاتون به اشتراک بذارم که بهتون کمک میکنه performance اپلیکیشن تون رو چند پله بالاتر ببرید. قطعا همه نکات رو نمیشه پوشش داد ولی در حد و اندازه ای که حوصله دارم میگم بهتون.

بریم بترکونیم
بریم بترکونیم


تعریف مساله

وقتی حد و اندازه پروژه شما از یه جایی بیشتر میشه! علاوه بر معماری اپلیکیشن، ساختار پروژه، فولدر بندی ها، جدا سازی ها، تکنولوژی ها و کتابخونه هایی که استفاده می‌کنید، باید همیشه به این نکته هم توجه کنید که کدی که می‌نویسید در بهترین حالت خودش کار کنه یا حداقل برای اینکه کار به اینجا برسه باید تلاش کنید. بحث performance همه جا مهمه! چه توی query هایی که سمت backend میخواد انجام بشه چه توی اپلیکیشن ری-اکت شما. یه تصور غلطی که متاسفانه خیلی دیدم اینه که خیلیا فکر میکنن فرانت-اند فقط باید نقش نمایش رو داشته باشه و بحث هایی مثل معماری و مهندسی و پرفورمنس و ... اینجا اصلا مطرح نیست! در صورتی که این موضوع اتفاقا با توجه به ماهیتش که در واقع ویترین کار بیزنس هست و جایی هست که کاربر مستقیما باهاش ارتباط داره بسیار بسیار نقش اساسی و مهمی داره.

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

مثالی که زدم خیلی سطحی روی مواردی تمرکز داشت که اهمیت تجربه کاربر رو از استفاده از یک سرویس به ما نشون بده. این اتفاقات میتونه دلایل خیلی زیادی داشته باشه ولی ما میخوایم اینجا در مورد این صحبت کنیم که چطور از چنین مواردی جلوگیری کنیم که تجربه کاربری بهتری داشته باشیم.

بحث تجربه کاربری که شد یه نکته ای هم دوست دارم اضافه کنم، اونم اینه که این موضوع بسیار گسترده هست و شامل موارد متعددی میشه. مثلا طراحی که انجام میشه چقدر user-friendly هست. آیا اندازه ها برای صفحه های کوچک و بزرگ بهینه شده؟ رنگ ها کنتراست خوبی دارن؟ نحوه جاگیری عناصر توی صفحه چطوره؟ و خیلی موارد دیگه. که ما اینجا به اون موارد کاری نداریم.

هر موردی رو که میگم سعی میکنم یه لینک به منابعش هم بذارم که راحت تر بتونید ایده بگیرید.

پس بزن بریم ببینیم در حد خودمون چیکار میتونیم بکنیم.


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

هر چیزی که به صورت استاتیک توی اپلیکیشن تعریف شده، مخصوصا تصاویر و آیکون ها رو تا جای ممکن کم حجم کنید که لود شدنش از کاربر وقت زیادی نگیره. استفاده از تصاویر خیلی بزرگ اصلا ایده خوبی نیست. بعضی از فریمورک ها مثل Next برای این موضوع یه فکرایی کردن، مثلا استفاده از کامپوننت Image که خود Next ارائه میده. در کنارش اگر از آیکون های png استفاده می‌کنید حتما به Sprite Sheet فکر کنید. اگر از SVG استفاده می‌کنید یه سری راه و چاه جلو تر بهش اشاره کردم که میرسیم بهش. در کنار این موارد میتونید از تکنیک هایی مثل Progressive Image Loading استفاده کنید.


تا جایی که میشه از Memo استفاده کنید

اینکار باعث میشه کامپوننت هایی که props یا state شون تغییری نکرده نسبت به قبل لازم نباشه دوباره رندر بشن (البته چیز هایی که باعث rerender شدن کامپوننت میشن ۳-۴ تا چیز هستن ولی بیشتر با این ۲ تا کار داریم).

فرض کنید یه Header دارید توی اپلیکیشن که همیشه prop هاش یکسانه و همینطور state هاش و حتی اصلا state نداره، و این کامپوننت رو چندجا استفاده کردید. هر جایی که کامپوننت والدش (Parent Component) دوباره رندر بشه (احتمال زیاد به خاطر تغییرات state) مجدد اون کامپوننت child هم رندر میشه. memo به شما کمک میکنه که این اتفاق نیوفته.

قبلا که استفاده از class component ها رایج تر بود، یه چیزی بود به اسم PureComponent که چیزی شبیه به همین کار رو انجام میداد. در واقع جزییاتش بیشتره که میتونید اینجا بخونید.

خیلی تشویق تون میکنم که کامپوننت های ثابت تون رو مثلا همون Icon ها رو زمانی که export میکنید memo هم انجام بدید.

مثلا:

import React from &quotreact&quot const SvgComponent = ({ size, color }) => ( <svg viewBox=&quot0 0 24 24&quot xmlns=&quothttp://www.w3.org/2000/svg&quot width={size} height={size} fill={color} > <path fill={color} fillRule='evenodd' clipRule='evenodd' d='M12 1.75c-1.742 0-3.943.12-5.731.242A4.592 4.592 0 001.992 6.27C1.87 8.057 1.75 10.259 1.75 12c0 1.742.12 3.943.242 5.731a4.592 4.592 0 004.277 4.277c1.788.122 3.99.242 5.731.242 1.742 0 3.943-.12 5.731-.242a4.592 4.592 0 004.277-4.277c.122- 1.788.242-3.99.242-5.731 0-1.742-.12-3.943-.242-5.731a4.592 4.592 0 00-4.277- 4.277C15.943 1.87 13.741 1.75 12 1.75zm4.522 8.038a.75.75 0 10-1.044-1.076l-5.146 4.995-1.811-1.747a.75.75 0 00-1.042 1.08l2.335 2.25a.75.75 0 001.043-.002l5.665-5.5z' ></path> </svg> ) // Use React.memo to memoize this component export default React.memo(SvgComponent);

این یه نمونه استفاده از React.memo هست. سعی کنید از این روش تو هر جایی که ممکنه استفاده کنید. برای درک عمیق ترش هم اینو بخونید.


کامپوننت های تنبل! (همون Lazy)

یکم هم فارسی بگم دیگه! Lazy Loading اصلا بحث جدیدی نیست ولی خیلی کاربردی و مهمه. مخصوصا از وقتی که این قابلیت رو خود React به کمک React.lazy و Suspense پشتیبانی میکنه. البته تنها مشکل اینجاست که lazy فقط برای لود کردن کامپوننت ها کاربرد داره.

مثلا تصور کنید کامپوننت UserInfo دارید که توی هدر زمانی که کاربر روی عکس پروفایلش کلیک میکنه توی هدر داخل یه sub menu نمایش داده میشه. پس نکته اینجاست که تا زمانی که کاربر روی اون عکس پروفایلش کلیک نکنه نباید این کامپوننت لود بشه! چه کاریه اصلا. اینجاس که lazy و Suspense به کارمون میاد.

یادتون باشه که React.lazy و Suspense در کنار همدیگه هستند که معنی پیدا میکنن. این نمونه رو ببین:

import React, { Suspense } from 'react'; // ... const UserInfo = React.lazy(() => import('./UserInfo')); const Header = () => ( <div className='header'> <Suspense fallback={<div>Loading...</div>}> <UserInfo /> </Suspense> </div> ); export default React.memo(Header);

یکم باز ترش کنیم باهم؟

  • همیشه و حتما ورودی React.lazy باید یه تابع باشه که dynamic import رو انجام بده.
  • کامپوننتی که Lazy هست و import شده رو حتما باید داخل Suspense استفاده کنید وگرنه کار نمیکنه.
  • برای تایمی که ممکنه این لودینگ طول بکشه میتونید از fallback که یک prop از Suspense هست استفاده کنید و یه لودینگ به کاربر نمایش بدید.

این کار در واقع یک نمونه از بحث Code Splitting هست که میتونید مفصل در موردش اینجا بخونید.

یه نمونه خیلی خوب دیگه استفاده از Lazy و Suspense تو جاییه که دارید همه Route ها رو تعریف می‌کنید که البته نمونه اش تو خود داکیومنت React هست که ۲ خط بالاتر لینکش هست.


دسته کردن setState ها (یا همون State Batching)

هر بار که مقدار یک state تغییر میکنه کامپوننت مجددا render میشه. خب اگه ما بیایم و تو یه فانکشن ۳ بار اینکارو انجام بدیم یعنی چی؟ ۳ بار rerender؟ بذارید یکم عمیق تر بررسیش کنیم. این نمونه رو ببین:

اینجا ۳ تا state داریم با اسمای شخمی که اصلا مهم نیست. مهم اینه که توی داریم ۳ بار setState انجام میدیم. اتفاقی که میوفته اینه که توی این حالت خاص React بعد از آخرین setState (اینجا میشه setC) با یکبار رندر کردن هر سه تغییر رو اعمال میکنه. عملا فقط یک بار setState شده و کامپوننت ۱ بار rerender شده.

خب این که خیلی هم خوبه! ولی مشکل اینجا بود که اگر این setState ها داخل یه callback یا promise بودن این اتفاق نمیوفتاد. یعنی اگه اینجوری بود:

تو این حالت، ۳ بار setState اتفاق میوفتاد و باعث میشد ۳ بار هم کامپوننت رندر بشه. ولی از React ورژن ۱۸ این مورد هم برطرف شده و حتی توی این حالت هم فقط ۱ بار کامپوننت رندر میشه. دمتون گرم بچه های React.

برای درک عمیق میتونید اینجا رو بخونید.


قابلیت جدید startTransition

خب یکی دیگه از قابلیت های React 18 همین API جدید startTransition هست. یکم اگه بخوام مقدمه چینی کنم، میتونم بحث Race condition رو بگم. مثال خیلی ساده اش میشه این:

فرض کنید یه ورودی داریم که کاربر قراره توش شروع کنه به تایپ کردن و طبق ورودی ای که کاربر داده ما نتایج جستجو رو بهش نشون بدیم. چیزی که کاربر میخواد بنویسه هست Box. طبیعتا اول B رو میزنه بعد o رو میزنه و بعد هم x رو میزنه.

حالا اتفاقی که ممکنه بیوفته اینه که کاربر نوشته bo و مقداری که الان توی input هست همین bo هست ولی نتیجه سرچی که ما داریم نشون میدیم بر اساس b هست. یعنی سرعت جواب و سرچ با سرعت تایپ کاربر همخوانی نداشته باشه و ایجاد یه تجربه بد بکنه. برای این مشکل خیلی راه حل ها وجود داره که مثلا یکیش استفاده از useDebounce هست. که در واقع یه تابع رو میگیره و یه تایمر و بعد از اینکه دیگه ورودی های اون تابع بعد اون تایم که بهش داده شده تغییر نکرد، شروع میکنه به اجرا شدن.

خب پس یه دید بهتری پیدا کردیم نسبت به مشکل و حالا ببینیم startTransition چطوری به کمک مون میاد:

این API به شما کمک میکنه که بدون اینکه UI به اصطلاح Block بشه کار های دیگه هم انجام بشه. یکی از مهم ترین کاربرد هاش هم همون بحث search input هست که قبل تر مثالش رو زدم. مثلا اگه قبلا مقدار state رو این شکلی تغییر میدادید:

setSearchValue(value);

الان میتونید اینشکلی انجامش بدید:

startTransition(() => { setSearchValue(value); })

از این طریق اپلیکیشن شما میتونه بدون اینکه بقیه کارارو ول کنه و مقدار state رو تغییر بده و ..... اینکار رو بهینه تر انجام بده. یکم توضیحش سخته! پیشنهاد میکنم خودتون امتحان کنید.


استفاده از useCallback و useEvent

توابعی که داخل بدنه کامپوننت تعریف میشن توی هر بار rerender شدن دوباره بوجود میان. نگاه کنید:

اینجا تابع بعد از هر بار تغییر (مثلا تغییر state) مجدد تعریف میشه. توی این نمونه wrap کردن این تابع توی useCallback باعث میشه که فقط یکبار این تابع تعریف بشه. اینطوری:

ولی....

اگه این تابع یه ورودی داشته باشه یا مثلا یه مقداری از state یا prop رو بخواد بخونه یا تغییری بده باید به عنوان dependency به useCallback پاس داده بشه و عملا هیچ تفاوتی ایجاد نمیکنه. اینجا جاییه که useEvent به کمکمون میاد! در موردش یه مطلب مفصل نوشتم که میتونید اینجا بخونیدش و همینجا این بحث رو تموم میکنم.


لیست های بزرگ رو Virtualize کنید

حتما پیش میاد که ممکنه یه لیست خیلی بلند و بالا از آیتم های یه لیست داشته باشید. مثلا لیست املاک، لیست اسامی،‌اسم کتاب ها، لیست غذا ها و ... ایده اینه که بجای اینکه بیایم همه آیتم های لیست رو یکجا رندر کنیم (و پدر صاحب بچه رو در بیاریم) یکم هوشمند تر عمل کنیم.

میتونیم از تکنیک های همیشگی مثل paginate استفاده کنیم ولی اگه دقت کرده باشید این روش رو دیگه زیاد نمیبینید و دیگه نم نم همه دارن میرن سمت infinite page ها که هر چقدر هم scroll میکنید باز هم دیتا براتوت لود میشه.

پس چطوره که بیایم و فقط آیتم هایی رو رندر کنیم که توی viewport کاربر هست و میتونه اونا رو ببینه و بقیه اش رو زمانی رندر کنیم که کاربر داره بهشون میرسه. برای همین یه کتابخونه وجود داره به نام react-window که دقیقا برای همچین سناریویی طراحی شده. توضیحش رو دادم و پیشنهاد میکنم که خودتون یه نگاهی بهش بندازید.


بیلد بگیرید

این مورد خداییش خیلی دیگه ساده است باید بدونید دیگه... هر موقع خواستید پروژه رو بفرستید روی سرور و به مرحله production رسید حتما قبلش یه دونه yarn build یا npm run build بگیرید که mode اپلیکیشن بره روی build که این باعث میشه حجم همه فایل ها خیلی کمتر بشه، فشرده تر بشه و خیلی چیز های دیگه. دیگه نمیگم در مورد این انتظار دارم خودتون بدونید اینو.



یه سری نکته اضافی که میسپرم به خودتون که سرچ کنید:

  • سعی کنی برای request ها و درخواست های سمت سرور از react query استفاده کنید که به شما قابلیت cache کردن دیتا ها رو میده، اینطوری کاربر وقتی بین صفحات و قسمت های مختلف حرکت میکنه دیتای اضافی رد و بدل نمیشه و یه چیزی داره که ببینه.
  • توی کدتون به بحث time complexity توجه کنید. اگه در موردش مثل من اطلاعات خیلی زیادی ندارید شروع کنید به یاد گرفتن و توابع مهم و critical تون رو از این نظر بررسی و اصلاح کنید.
  • کتابخونه های حجیم که خیلی ممکنه network رو درگیر کنه رو فقط زمانی توی پروژه import کنید که اپلیکیشن رندر های اصلیش رو انجام داده باشه و کاربر معطل یه چیز بیخود نشه. نمونه اش میتونه کتابخونه هایی مثل sentry و hotjar باشن.
  • اگر برای دیباگ کردن نیاز به کمک دارید React Profiler رو ببینید و باهاش کار کنید.
  • حتما بحث core web vitals رو جدی بگیرید و برای بهبود اون متریک ها تلاش کنید. بحثش خیلی مفصله و شاید در آینده یه مطلب در موردش نوشتم.
  • از تایپ اسکریپت استفاده کنید. شاید مستقیما تاثیری توی performance نداشته باشه ولی استفاده کردن ازش در کل خیلی کمک میکنه.



تجربه کاربریreactتایپ اسکریپتperformance
۹۶
۱۲
حسان امینی لو
حسان امینی لو
برنامه نویس از جلو
شاید از این پست‌ها خوشتان بیاید