توی این مقاله قراره کلی نکات که برای بهتر کردن تجربه کاربری و بالا تر بردن پرفورمنس توی پروژه های ری اکتی لازمه رو یاد بگیریم
با خوندن این مقاله هم با یه بخش دیگه از هوک های کاربردی ری اکت آشنا میشید تا کلا پرونده هوک ها که تو قسمت اول بخشی ازش رو شروع کردیم بسته شه، هم بعضی قابلیتای ورژن جدید ری اکت رو یاد میگیرید تا آپدیت شید، هم کلی نکات پرفورمنسی یاد میگیرید.
این یکی از باحال ترین هوک هاییه که توی ورژن جدید معرفی شده و واقعا کاربردیه!
اگه بخوام خیلی ساده توضیحش بدم با یه مثال شروع میکنم:
فرض کنید یک لیست هزارتایی از محصولاتتون دارید که توی یه صفحه نمایششون میدین، و یه اینپوت سرچ دارید که با سرچ کردن اسم محصول، اون هزارتا محصول رو فیلتر میکنید ، با وارد کردن هر حرفی توی اینپوت اون پروسه ی فیلتر کردن هزارتا محصول دوباره از سر گرفته میشه تا دیتا رو بر اساس اون اسم سرچ شده آپدیت کنه، حالا مشکلی که بهش برمیخوریم اینه که چون این پروسه ی فیلتر کردن سنگینه اگه شما بخواید سه چهارتا کلمه رو پشت سر هم بنویسید متوجه یه تاخیر سنگین تو وارد شدن اون حروف به اینپوت و یه لگ خیلی سنگین میشید(هرچی دیتا بزرگ تر، لگ سنگین تر).
اگه تا حالا همچین چیزی رو تجربه نکردین حتما به دمویی(لینک دمو) که برای مثالمون آماده کردم یه سر بزنید و سعی کنید مثلا 4444 رو سرچ کنید تا محصولات فیلتر شن و بعدش 4444 رو یکجا پاک کنید تا متوجه لگ یا تاخیر در پاک شدن که دربارش حرف میزدم بشید
توی ری اکت این پروسه به این شکل هستش: ما یه استیت داریم که مقدار اینپوت رو داخلش ست میکنیم (با وارد کردن هر حرف این استیت اپدیت میشه و عملا مقدار داخل اینئوت هم آپدیت میشه) و یه استیت داریم که اون هزار تا محصولاتمون داخلشه و برای اینپوت یه هندلر () میزاریم که با وارد کردن حروف اون دیتا رو فیلتر کنه و استیت محصولاتمون رو آپدیت کنه :
حالا دلیل پشت پرده ی این لگ یا تاخیر چیه که باهاش مواجه شدیم:
ری اکت همه ی استیت هارو سعی میکنه به یکباره اپدیت و کامپوننت رو رندر بکنه، ینی میاد همه ی استیت هارو اپدیت میکنه اما استیت کوئری زودتر اپدیت میشه چون پروسه پیچیده و خاصی نداره اما با اینکه آپدیت شده ری اکت نمیفرستتش تو صفحه تا فعلا پروسه آپدیت شدن استیت محصولات هم تموم شه و بعد که تموم شد تازه جفتشو باهم ری رندر و پینت میکنه رو اسکرین و با این تفاسیر همه ی استیت ها برای آپدیت شدن از یه درجه اولویت بهره مندند(همه ی استیت ها در ری اکت بالاترین اولویت را دارند) و ری اکت پارتی بازی قائل نمیشه! و در نتیجه یکبار صفحه رو ری رندر میکنه که همه ی استیت های اپدیت شده توش باشن.
نکته: برای حل این مشکل لگ زدن و تاخیر ما توی ورژن های قبلی از روش هایی مثل پجینیشن (pagination)و... استفاده میکنیم تا دیتارو محدود کنیم تا در نتیجه موقع سرچ و فیلتر کردن فقط تعداد خاصی که توی صفحه اول نشون میدیم رو فیلتر کنه و خیلی سنگین نشه پروسه اش.
حالا توی ورژن جدید ری اکت، این پارتی بازی با استفاده از هوک "useTransition" ممکن شده و شما میتونید در کنار پچینیشن با این هوک یه زیرمیزی هم به ری اکت بدین تا دیرتر بره سراغ فلان استیت و اول کار بقیه استیت هارو راه بندازه. ینی چی؟
ما میخوایم استیت هایی مثل استیت کوئری برای اینپوت که اصلا نیازی به زمان و پروسه خاص ندارن خیلی سریع آپدیت بشن و برن تو اسکرین تا مجبور نباشن منتظر آپدیت شدن بقیه استیت ها بمونن و استیت هایی مثل فیلتر کردن محصولات که خیلی زمان میبرن اولویت پایینتری داشته باشن در نتیجه این کار باعث میشه ری اکت چند بار ری رندر بکنه کامپوننت رو (یکبار برای استیت هایی که در اولویت بالا هستند اپدیت و ری رندر میشه تا فعلا کار اونا راه بیفته و در عین حال از اونور داره همزمان پروسه ی کم اولویت هارو پیش میبره تا بعد از اتمام پروسه با یه ری رندر دیگه اونارو هم بفرسته تو صفحه) به این قابلیت چند رندری همزمان که توی تازه ترین ورژن ری اکت اضافه شده، concurrent rendering میگویند.
توی مثال دنیای واقعی مثل این میمونه که من امروز باید این مقاله رو بنویسم و در عین حال باید غذا هم بخورم پس آیا درسته که مقالمو با اینکه نوشتم تموم شده ولی منتشرش نکنم تا غذام بپزه و بعد تازه شروع کنم به خوردن و بعد از تموم کردن غذام دکمه انتشار رو بزنم که جفتش هم زمان تموم شده باشه؟! نه خب من با ویژگی concurrent میتونم مقالم رو بنویسم و در عین حال غذا که اولویت پایینتری داره رو بزارم بپزه و من مقالم به محض تموم شدن منتشر میکنم و و بعدش غذام هم که هر وقت آماده شد میخورمش. منطقی بنظر میرسه مگه نه؟ اینطوری کلی " سریع تر " کارام انجام میشه و تجربه بهتری بدست میارم.
نکته: برای اینکه ری اکت بتونه از قابلیت چند رندری همزمان استفاده کنه باید آخرین ورژن ری اکت استفاده بشه و method ReactDom به شیوه جدیدش نوشته شده باشه:
حالا useTransition چطور استفاده میشه؟
این هوک یه آرایه با دو مقدار برمیگردونه که اولیش isPending و دومیش StartTransition هستش:
مقدار isPending برابر با true یا false هستش و تا زمانی که اون استیت اولویت پایین هنوز کامل آپدیت و رندر نشده این مقدار true هستش و ازش میتونیم برای لودینگ استفاده کنیم به این صورت که اگه isPending بود بجای محصولاتمون کلمه loading رو نشون بدیم و تجربه کاربری بهتری داره.
مقدار startTransition یه فانکشن هست که یه کال بک میگیره که داخلش ست استیت هایی که اولویت پایین تری دارن رو قرار میدیم تا ری اکت متوجه بشه که این استیت ها اولویت پایین تری دارند و نباید بقیه رو بخاطر این استیت ها منتظر بزاره. به این صورت:
حالا برید این نسخه دمو ( لینک دمو ) رو امتحان کنید تا ببینید چقدر خفن تر و بهتر شده دیگه خبری از لگ و تاخیر موقع یهویی پاک کردن اینپوت نیست و یه لودینگ هم داریم که وضعیت پروسه فیلترینگ رو به ما نشون میده:
نکته: یادتون باشه فقط از useTransition برای همچین مواقعی استفاده کنید و سعی نکنید هر چیز بیخودی رو اولویت بندی کنید چون ری اکت با همزمان سازی ری رندر ها فشار خاصی رو محتمل میشه که ممکنه اون اولویت بندی شما ارزش این فشار رو نداشته باشه و در آخر نتیجه معکوس داشته باشه برای پرفورمنس!
این هوک دقیقا مثل هوک useTransition هستش منتها فرقشون اینه که از useTransition وقتی استفاده میکنید که داخل کامپوننتمون به setState دسترسی داشته باشیم و اما مواقعی هست که ما استیت رو از کامپوننت پدر به صورت پراپ گرفتیم و به ست استیتِ اون پراپ دسترسی نداریم پس بجاش میایم از هوک useDeferredValue استفاده میکنیم که فقط یه مقدار قبول میکنه و اون مقدار چیزی نیست جز همون استیتی که میخوایم اولویت پایینی داشته باشه:
این کد بالا در نتیجه هیچ فرقی با کد های بالاتر که که با استفاده از useTransition بود نداره و جفتشون کارشون رو به نحو احسنت انجام میدن البته در این حالت دیگه isPending رو نداریم.
تصور کنید یه کامپوننت داریم به این صورت:
یه فانکشن greetingFunc داریم که پروسه حلقه ایِ سنگینی رو داره انجام میده و در نتیجه یه مقداری رو برمیگردونه حالا ما یه متغیر به اسم greeting داریم که میاد این فانکشن رو صدا میکنه و استیت اسم رو بهش پاس میده و اون پروسه انجام میشه و مقدار متغیرمون برابر میشه با مقدار خروجیِ اون فانکشنمون حالا یه تم هم داریم که از استیت دیگه ای داره یه شرطی اجرا میکنه که اگه ترو بود بیاد مشکی کنه استایلو.
حالا اگه ما هر سری بخوایم این تم رو با زدن دکمه ی change theme عوض کنیم بخاطر آپدیت شدن استیت darkTheme ، کل کامپوننت ری رندر میشه و اون متغیر greeting دوباره اون فانکشن رو صدا میکنه و یه پروسه سنگین رو انجام میده و باعث محتمل شدن یه بار سنگین به پرفورمنس میشه. حالا چیزی که شاید متوجهش شده باشید اینه که این بار اضافی سرِ هیچی اضافه شده! وقتی ما داریم تم رو عوض میکنیم استیت اسم که داریم پاسش میدیم به فانکشن عوض نشده و در نتیجه فانکشن همون خروجی رو میده. پس وقتی همون خروجی رو میده و اسم همونه چرا باید این فانکشن از اول اجرا شه و اون پروسه سنگین تکرار شه؟
اینجاس که useMemo میاد وسط و با استفاده ازش میتونیم این فانکشن رو به یاد بسپاریم و اگه اون دپندسی که ما بهش میدیم تغییری نکرده باشه این فانکشن دوباره اجرا نمیشه و همون خروجی قبلی رو میده(چون دپندسنی که اینجا استیت name باشه تغییری نکرده).
هوک useMemo یه کال بک میگیره که توش فانکشنی که میخوایم به یاد سپرده بشه رو ریترن میکنه و به عنوان آرگومان دوم یه آرایه قبول میکنه که توش میتونیم دپندنسی هامون رو بزاریم تا فقط هروقت که این دپندسی ها تغییر کردن این فانکشن از دوباره اجرا بشه و اصلا با این قضیه که "کامپوننت داره ری رندر میشه پس منم از دوباره فانکشن اجرا میکنم" کاری نداره و فقط چشمش به دپندنسی هاس.
مثال بالا رو با هوک useMemo به اینصورت مینویسیم:
مثال بالا رو میتونید توی دمو ( لینک دمو ) امتحان کنید یکبار با هوک سعی کنید تم رو عوض کنید و یکبار بدون هوک useMemo تا ببینید متن 'greetingFunc has been created again ' با عوض شدن تم دوباره لاگ میشه یا نه.
ما به دو دلیل از useMemo و useCallback استفاده میکنیم:
برابری ارجاعی یا Referential equality
محاسبات پر هزینه یا Computationally expensive calculations
در مورد مورد دوم حرف زدیم که چجوری با useMemo میتونیم از تکرار محاسبات سنگین جلوگیری کنیم( با به یاد سپردن خروجی اون محاسبات)
هوک useCallback هم وظیفه هندل کردن مورد اول رو داره، بیاید با مثال جلو بریم:
مثل چیزی که تو عکس بالا میبینید بعضی وقتا پیش میاد که ما یه فانکشن رو به عنوان پراپ پاس بدیم به کامپوننت فرزند تا ازش استفاده کنه حالا فکر کنید وقتی استیت رو توی کامپوننت پدر با دکمه increase آپدیت کنیم چه اتفاقی میفته؟ => فانکشن که به عنوان پراپ پاس داده شده به کامپوننت dummyButton از نو ساخته میشه و وقتی از نو ساخته بشه حتی اگه شبیه قبلی هم باشه با قبلی یکی نیست! توی جاوااسکریپت وقتی یه فانکشن یا آبجکت میسازیم توی یه آدرسی ذخیره میشه و وقتی یه آبجکت دقیقا کپی همون ساخته شه با این که شکلاشون یکیه ولی با یه آدرس جدا ساخته شده که این قضیه پدیده ی Referential equality رو به وجود میاره. با ساخته شدن فانکشن جدید چون پراپ تغییر کرده پس کامپوننت فرزند ( DummyButton) هم تغییر میکنه اینجاس که پای useCallback میاد وسط و ما کافیه فانکشنمون رو به عنوان کال بک پاس بدیم بهش و و به عنوان ارگومان دوم هم یه آرایه از دپندسی بدیم که هر وقت دپندسی تغییر کرد اون فانکشن رو دوباره بسازه و در غیر این صورت همون فانکشن رو با همون آدرس خودش به یاد میسپره و وقتی با همون آدرس به یاد سپرده شده باشه دیگه تغییری هم صورت نگرفته که باعث ری رندر شدن بیخودِ کامپوننت فرزند بشه و پرفورمنس خوشحال تره!
مثال بالا، با استفاده از useCallback به صورت زیر نوشته میشه و شما میتونید با رفتن به دمو ( لینک دمو ) همین مثال رو یبار با هوک و یبار بدون هوک تست کنید تا ببینید وقتی استیت با دکمه increase تغییر میکنه آیا باعث ری رندر شدن dummyButton و لاگ شدن متن ' dummy button component rerendered ' میشه یا نه.
نکته: فرق useMemo و useCallback اینه که useMemo اون مقدار برگشتی از داخل فانکشن رو به یاد میسپره تا دوباره فانکشن اجرا نشه، ولی useCallback خود فانکشن رو به یاد میسپره تا دوباره فانکشن ساخته نشه.
نکته : از این دو هوک فقط و فقط سعی کنید توی این دو شرایط برابری ارجاعی و محاسبات سنگین استفاده کنید و در غیر اینصورت اگه بخواید هر فانکشن کوچیکی که مینویسید رو memoize یا به یاد سپاری کنید ممکنه نتیجه معکوس بده و بخاطر یه محاسبه کوچیک بی ارزش ری اکت کلی درگیر اجرای useCallback و useMemo بشه و بدتر پرفورمنس رو درگیر و سنگین کنه.
وقتی توی کامپوننتمون یه سری کامپوننت فرزند داشته باشیم با هر بار ری رندر شدن کامپوننت پدر ، تمام کاپوننت های فرزند هم ری رندر میشند. اما بدبختی که روی پرفورمنس میاره همینه که این فرزند ها با اینکه پراپ هاشون هیچ تغییری نکردن باید ری رندر بشن فقط بخاطر اینکه پدرشون ری رندر شده! و این واسه پرفورمنس خوب نیست...
ما باید ری اکت رو مجبور کنیم تا قبل از ری رندر کردن هر کامپوننت بیاد پراپ جدیدش رو با پراپ قبلی مقایسه کنه و اگه فرق داشتن اجازه داشته باشه اون کامپوننت رو ری رندر بکنه و این کار فقط به وسیله react.memo امکان پذیره. memo یه کال بک میگیره که اون کال بک همون کامپوننت ماس که میخوایم هر سری که پدرش ری رندر میشه پراپش چک بشه و در صورت تغییر پراپ این هم ری رندر بشه وگرنه منو سنه نه که پدرم ری رندر شده!
توی مثال بالا چون از react.memo استفاده نشده با هر بار اپدیت شدن استیت در کامپوننت App ، کامپوننت ChildComponent هم ری رندر میشه با اینکه هیچ ربطی به استیت نداره و این بنده خدا اصلا حتی پراپ هم نمیگیره که بخواد تغییری هم داشته باشه! پس واسه بهتر کردنش به این شکل از react.memo استفاده میکنیم:
میتونید با رفتن به دمو ( لینک دمو ) مثال بالا رو خودتون با memo و بدون اون امتحان کنید تا ببینید با اپدیت شدن استیت در کامپوننت App آیا کامپوننت فرزند ری رندر شده و متن ' child component get re rendered again ! ' لاگ میشه یا نه.
ما وقتی میخوایم توی یه صفحه چند تا فایل کامپوننت اضافه کنیم با استفاده از import اینکارو میکنیم که به صورت کاملا استاتیک و موقع کامپایل کردن همه ی اون فایلارو ایمپورت میکنه تو فایلمون و تحویل میده و عملا ما هیچجوره نمیتونیم بگیم که اگه مثلا فلان شرط برقرار بود بیا این کامپوننت رو لود کن وگرنه اگه اون شرطه برقرار نشه اصلا چرا باید اینو لود کنم و حجم رو ببرم بالا و وقت کاربر هم الکی گرفته شه؟
بزرگترین موقعیتی که میتونیم با این مشکل مواجه شیم وقتیه که داریم مسیرای مختلف رو توی فایلمون میدیم و برای هر مسیر یه کامپوننت خیلی بزرگ برای رندر شدن توی هر آدرس مشخصی ایمپورت میکنیم خب بهترین چیزی که میتونه اتفاق بیفته اینه که این کامپوننت ها به صورت داینامیک و فقط وقتی مورد نیاز هستن و احضار شدن لود بشن و تا وقتی لازمشون نداریم الکی تایم کاربر سر لود شدن چیزی که قرار نیست حتی چشمش بهش بیفته هدر نره و اینجاس که قابلیت code splitting به وسیله lazy و suspense به دادمون میرسه!
این دو دقیقا کارشون همینه که کامپوننت رو در صورت نیاز لود کنن و تا وقتی نیازی نیست زمان سر لود شدن اون کامپوننت ها مصرف نشه تا پرفورمنس سریع تری داشته باشیم موقع لود شدن صفحات.
همونطور که توی مثال بالا میبینید قراره هر کامپوننت سر موقعِ نیاز ( عوض شدن مسیر) لود بشه مثلا وقتی مسیر / هستش کامپوننت Home لود بشه وقتی مسیر به panel تغییر پیدا کرد کامپوننت Panel لود بشه و...
متد lazy یه کال بک میگیره که این کال بک یه ایمپورت برمیگردونه که داخل ایمپورت مسیر کامپوننتمون رو میزاریم ( خط 5 و 6 مثال بالا)
همه ی کامپوننت هایی که با lazy ایمپورت شدن باید داخل suspense قرار بگیرند و fallback ای که به ساسپنس به صورت پراپ داده شده در واقع یه jsx هستش که در فاصله زمانی که کامپوننت در حال لود شدن هستش نشون داده میشه تا حس بهتری به کاربر منتقل کنه.
فرض کنید از سرور صد تا عکس گرفتید و قراره توی صفحه به کاربر نشون بدید، وقتی کاربر وارد صفحه بشه تک به تک اون صد تا عکس(شایدم بیشتر!) باید لود بشن درحالی که کاربر حتی ممکنه ده تا از اون عکسا هم اسکرول نکنه و نبینه و فقط دوسه تای اول رو ببینه. خب پس در این صورت چرا باید اون عکسهایی که هنوز دیده نشدن و نیازی به لود شدن ندارن وقت مارو بگیرند و وب سایت مارو کند تر کنند؟
اینجاس که لایبرری به شدت کاربردی و دوست داشتنی React Lazy Load Image Component میاد وسط و کارش اینه که بیایم بجای تگ img از این استفاده کنیم تا فقط عکس هارو در صورت نیاز لود بکنه و در نتیجه http ریکوئست های کمتری زده شه و پرفورمنس بهتری داشته باشیم و هم اینکه میتونیم قابلیت باحالی مثل placeholder یا effect برای عکسمون داشته باشیم تا زمانی که عکس سنگینه و هنوز کامل لود نشده یه حالت بلور رو نمایش بده یا حالا عکسی که ما توی placeholder قرار میدیم. از این کامپوننت به این شکل استفاده میکنیم:
میتونید مثالای بیشتر و داکیومنت کامل این لایبرری رو با سرچ اسمش تو گوگل مطالعه کنید.
خب اینا نکات پرفورمنسی بود که اگه درست ازشون استفاده کنیم میتونیم خیلی تجربه خوبی رو برای کاربر هامون رقم بزنیم و همچنین ری اکت دولوپر بهتری باشیم.
از این قسمت هم به عنوان نکات پرفورمنسی میشه یاد کرد و هم قسمت دومِ یادگیری هوک ها در ری اکت.
خدانگهدار و موفق باشی ?