Frontend team leader @snappfood
حذف Rerender های اضافی در کامپوننت های React
من با Angular.js شروع کردم (version 1). اصلا اون موقع هنوز ری اکتی با این هیبت وجود نداشت. ورژن های جدید angular هم زاده نشده بودن. Vue و امثالهم هم بعدها به بازار اضافه شدن. بنابراین، اون سالها شاخ ترین Framework برای Frontend همین angular ورژن یک بود. توسط گوگل هم پشتیبانی شده بود و خیال همه راحت که بهترین انتخاب ممکن هست. تنها رقبایی که براش وجود داشتن، Ember و Backbone بودن. این دو تا هم طرفدارای خاص خودشون رو داشتن ولی واقعا در حد Angular نبود. یکی از مشکلاتی که خود من با Angular داشتم، این بود که وقتی یه پروژه ی با Scale بالا رو باهاش میزدی، از یه جایی به بعد، کندی رو میدیدی. اصلا مشخص بود دیگه داره به مرحله ی زایش میرسه. یه مدت که گذشت، React اومد تو بورس. نقطه ی قوتش هم چیزی نبود جز Virtual Dom. آقا ما که با این مشکلات Angular آشنا بودیم، ندیده و نشناخته عاشق Virtual DOM و ری اکت شدیم. همین موقع ها هم بود که Angular V2 به بالا وارد شدن. اما به خاطر وجود نداشتن این ویژگی، واقعا چشم و دل ما براش نرفت. حتی تا وقتی Angular V4 معرفی نشد من در مورد سینتکس هاشم نخوندم.
اما بنا بر تقدیر، ما اول با Angular نسل جدید شروع کردیم. یه Framework کامل و غول که همه چیزی داشت. اصلا هرکاری بخوای باهاش بکنی میشه. منتهی یکمی زیادی کامله و واقعا برای اپلیکیشن هایی که برای یه بیزنس با تعداد مشتری های بالا و اپلیکیشن large Scale هست، خیلی انتخاب جالبی نیست. چون بحث اصلی تاپیک نیست زیاد وارد جزییات نمیشم، همینقد بگم من با Angular و React اپ های خیلی بزرگی زدم، پس بعد از یه تجربه ی بزرگ دارم همچین جمله ای میگم. بگذریم!
با وجود React و قابلیت مهمش، دیگه با خیال راحت میشد رفت سراغ پیاده سازی. خب وقتی ندیده و نشناخته، میری سراغ یه اپلیکیشن Large Scale و خیالت راحته که قراره یه Performance عالی تحویل بدی، یهویی میرسی به یه نقطه ای که میبینی کندترین اپلیکیشن ممکن رو ساختی! حالا ببینیم چرا؟
این قابلیت (Virtual DOM) برای خودش یه سری قواعد و قوانین داره. به هر حال مقایسه هایی که بین آپدیت های مختلف صورت میده، همه شون به یک شکل نیست. یه جاهایی این مقایسه ها روی Reference صورت میگیره، یه جاهایی روی Value. ممکنه شما به Debugger تون نگاه کنید و ببینید، آپدیتی روی Value ها ندارید ولی مثل ضربان قلب گنجشنک، کامپوننتت داره update میشه.
این آپدیت شدن ها، هرکدوم ممکنه دلایل خاص خودش رو داشته باشه، اما من امروز میخوام در مورد Memo کردن یه کامپوننت صحبت کنم. یکی از قابلیت هایی که با هوک ها معرفی شد، useMemo هست. این قابلیت به شما این امکان رو میده که کامپوننتتون رو داخل یه wrapper به نام React.memo رپ کنید، یه فانکشن کاستوم بهش بدید و براش تعیین کنید که در چه شرایطی میخواین کامپوننتتون آپدیت بشه. بریم ببینیم چجوری:
اینجا شما یه نتیجه از Profiler در کروم میبینید. یکی از صفحات اپلیکیشن من هست که وقتی درشون اکشن هایی رو انجام میدید، این مقدار ریرندر و time consuming وجود داره
کامپوننت Header کارت من، بیشترین میزان Rerender رو داشته در حالی که در این تستی که من گرفتم، هیچ کاری باهاش نداشتم. یه اکشنی رو روی یه کامپوننت دیگه توی Page انجام دادم و چون فقط یه state در Parent این کامپوننت ست شد، و کامپوننت پدر، Rerender شد، این کامپوننت و تمام بچه هاش، یه بار دیگه رندر شدن. خب دلیلش چیه؟
یکی از دلایلی که وجود داره اینه که ممکنه رفرنس آبجکت یا آرایه ای که به این کامپوننت پاس دادین، عوض شده باشه. برای همین میتونیم از React.memo توی این کامپوننت استفاده کنیم. به این صورت :
function HeaderCard(props){
...
return <div>
...
</div>
}
function areEqual(prevProps, currProps) {
return prevProps.title === currProps.title
}
export default React.memo(HeaderCard, areEqual)
توی این wrapper ای که برای کامپوننت میزارم، یه فانکشن پاس میدم به اسم areEqual. این تابع، بعد از هر بار درخواست رندر جدیدی که میاد، یه بار کال میشه و مقدار قبلی Prop ها و مقدار فعلی رو در اختیارتون میزاره. شما میتونید در بدنه ی این تابع، یه سری شروط بنویسید که فقط در صورت true شدن اون شرط ها، کاپوننتتون آپدیت و در نهایت Rerender بشه. مثلا در این کامپوننت من میخوام فقط وقتی title آپدیت شد، Rerender صورت بگیره. برای همین شرطم به این شکله که تا زمانی که مقادیر قبل و بعد title با هم برابر هستن، یعنی Prop ها با هم برابرن و نباید ریرندر انجام بشه.
این از مرحله ی اول. اگر کالبکی به کامپوننت خودتون پاس نداده باشین، همین کار کافیه و همین عمل باعث میشه که ریرندر اضافی از دوش کامپوننت برداشته بشه.
اما خیلی از دفعات هست که شما به instance کامپوننتون، یه کالبک پاس میدید. React.memo فقط میتونه جلوی آپدیت های بی مورد Prop ها رو بگیره. اما با هر بار آپدیت شدن Parent یه instance جدید از کالبکی که به کامپوننت دادید ساخته میشه و باعث کال شدن مجدد کامپوننت میشه. برای حل این مشکل، این بار باید از useCallback استفاده کنید:
<HeaderCard
...
onFaveClick={memoizedAddToFave}
Comment={memoizedClickComment}
Information={memoizedClickInformation}
/>
اینجا، بعد از پاس دادن Prop ها به کامپوننت، سعی میکنیم، Function ها رو هم Memoize کنیم. حالا ببنیم منظور از این چیه؟
const memoizedClickInformation = useCallback((x) => {
callFunctionA(x)
}, [y])
ما میخوایم وقتی داخل کامپوننت، تابع Information کال شد، تابع callFunctionA رو با ورودی x که از کامپوننت بهش داده میشه، کال کنیم. این کار با استفاده از useCallback به شکل بالا انجام میشه. اون مقداری که در آرایه به عنوان آرگومان دوم به useCallback پاس داده شده، یعنی [y] به این معنی هست که این تابع من، به مقدار y ، وابسته است یا اصطلاحا dependency داره. پس هر وقت مقدار y آپدیت شد، این کالبک هم باید آپدیت بشه و یه instance جدید ازش ساخته بشه. مثلا فرض کنید x یکی از attr های آبجکت y هست. اگر این dependency رو انجام ندید، ممکنه مقدار x در آبجکت y تغییر کنه اما وقتی کالبک صدا زده میشه، مقدار قبلی x رو دریافت کنید.
خب حالا ببینیم با این کار، نتیجه چه تغییری میکنه:
میبینید که در شرایط مشابه، نسب به حالت قبل، تمامی ریرندرهای اضافی حذف شد و به صفر رسید.
در پایان اینو بگم که ، این کار در حالت کلی توصیه نمیشه. چون باعث Memory usage زیادی میشه و فقط در مواقع Critical باید ازش استفاده بشه. از طرفی اگر Component Composition درستی وجود داشته باشه و معماری اپتون از اول درست باشه، این اتفاق نمی افته. اما یه زمانایی هم اجتناب ناپذیره. موفق باشید.
مطلبی دیگر از این انتشارات
یک مقایسه نا به جا: Redux یا React Context؟(بخش اول)
مطلبی دیگر از این انتشارات
مثال کاملی از پیاده سازی یک برنامه وبی پایه بر React
مطلبی دیگر از این انتشارات
چرا React ساخته شد؟