react در بیشتر مواقع عالی و سریع عمل میکند. اما گاهی اوقات به دلیل محاسبات سنگین سرعت آن کاهش مییابد، آن وقت است که برای جلوگیری از رندرهای بیهوده باید کامپوننتهای خود را بررسی و بهینه کنیم.
بهینه سازی هزینههایی به دنبال دارد. اگر به درستی انجام نشود، وضعیت ممکن است حتی بدتر هم شود. در این مقاله با روند رندرینگ آشنا میشویم و علت رندرهای بیهوده و راه حلها و چگونگی خراب شدن آن را میآموزیم.
فهرست مطالب
1. رندرینگ چیست؟ بررسی اجمالی فرآیند و مراحل آن
2. رفتار استاندارد رندر - علت هدر رفتن آنها در فاز رندر
3. بهبود عملکرد رندر - برخی از تکنیکها
4. چگونه ارجاعات جدید Propها بهینه سازی را از بین میبرند - جزئیات مشکل
5. بهینه سازی منابع propها: useMemo و useCallback
رندرینگ چیست؟
رندرینگ نوعی فرآیند در react است که از کامپوننتهای شما میخواهد آنچه که از ترکیب همزمان propها و state در رابط کاربری به وجود میآید را توصیف کنند.
بررسی اجمالی فرآیند
در طی این فرآیند، react از ریشه درخت کامپوننت شروع میشود و به سمت پایین حلقه میزند تا کامپوننتهایی را که به عنوان نیاز برای به روزرسانی نشانه گذاری میشوند، پیدا کند. برای هر یک از کامپوننتهای نشانه گذاری شده، ()render (برای کامپوننتهای کلاس) یا ()FunctionComponent (برای کامپوننتهای تابع) را فراخوانی کرده و خروجیهای رندر را ذخیره میکند.
خروجی رندر یک کامپوننت با JSX نوشته شده است. در صورت استفاده از render() یا ()FunctionComponent، در نهایت خروجی به عنصر react (ReactElement) تبدیل میشود که این عناصر در کنار هم برای تشکیل درخت مجازی استفاده میشوند.
پس از جمع آوری درخت جدید، react آن را متفاوت میکند. به این صورت که لیستهایی از همه تغییرات را که باید اعمال شود جمع میکند تا درخت واقعی مانند خروجی مورد نظر فعلی به نظر برسد. این روند Reconciliation نامیده میشود.
در بالا فرآیند بسیار اساسی برای ایجاد درخت میزبان (خروجی درخت) وجود دارد. درخت میزبان میتواند انواع مختلفی داشته باشد و بر پایه پلتفرمهای مختلف (وب، تلفن همراه و ...) باشد. دن آبراموف در اینجا توضیح بسیار خوبی برای آن شرح داده است.
تیم react فرایند بالا را به 2 مرحله تقسیم میکند:
- "فاز Render" شامل تمام کارهای رندرینگ کامپوننتها و محاسبه تغییرات است.
- "فاز Commit" فرآیند اعمال تغییرات در درخت میزبان است.
لایههای بیشتر React Native
توجه: اگر علاقهای ندارید میتوانید از این مرحله صرف نظر کرده و به خواندن ادامه دهید.
react native یک سلسله مراتب درختی را برای تعریف لایه اولیه انجام میدهد و یک تفاوت از آن درخت را در هر تغییر مانند طرح بالا ایجاد میکند. کامپوننت react native به روزرسانیهای رابط کاربری را از طریق چند لایه معماری مدیریت کرده و در پایان نحوه نمایش را نشان میدهد.
1. موتور لایه بندی Yoga
یوگا یک موتور لایه بندی کراس پلتفرم است که با زبان C نوشته شده و flexbox را از طریق اتصال به نماهای محلی (Java Android Views / Objective-C iOS UIKit) پیاده سازی میکند.
تمام محاسبات چیدمان نمایشها، متنها و تصاویر مختلف در react native از طریق یوگا انجام میشود. این اساسا آخرین مرحله قبل از نمایش بر روی صفحه است.
2. درخت سایه / گرههای سایه
وقتی react native دستوراتی را برای رندر لایهها ارسال میكند، گروهی از گرههای سایه برای ساختن درخت سایه مونتاژ میشوند كه نمایانگر سمت بومی لایه است، سپس به نمایش واقعی روی صفحه (با استفاده از یوگا) ترجمه میشود.
3. ViewManager
ViewManger واسطی است که میداند چگونه انواع View ارسال شده از جاوااسکریپت را به کامپوننتهای رابط کاربری بومی آنها ترجمه کند. ViewManager همچنین میداند که چگونه یک گره سایه و یک گره view بومی ایجاد و آنها را به روز کند. در فریمورک react native، ViewManager زیادی وجود دارد که استفاده از کامپوننتهای بومی را امکان پذیر میکنند. اگر به عنوان مثال روزی خواستید یک نمای سفارشی جدید ایجاد کنید و آن را به react native اضافه کنید، این view باید رابط ViewManager را پیاده سازی کند.
4. UIManager
UIManager آخرین قطعه از معما یا در واقع اولین مورد است. دستورات JavaScript JSX که به عنوان دستورات اجرایی به react native میگوید چگونه میتواند نمایشها را بصورت تکراری گام به گام تنظیم کند، به نیتیو ارسال میشود. بنابراین به عنوان اولین رندر، UIManager برای ایجاد ویوهای لازم دستور ارسال میکند و متناسب با تغییرات رابط کاربری برنامه که با گذشت زمان متفاوت است، به روزرسانی ارسال میکند.
بنابراین react native هنوز از توانایی react برای محاسبه تفاوت بین نمایش رندر قبلی و فعلی استفاده کرده و بر این اساس رویدادها را به UIManager ارسال میکند.
رفتار استاندارد رندر
رفتار پیش فرض react این است که وقتی کامپوننت والد رندر میشود، react به صورت بازگشتی تمام کامپوننتهای فرزند را نیز درون آن رندر میکند.
به عنوان مثال ما یک درخت کامپوننت به صورت A> B> C داریم.
- یک رندر مجدد را در B راه اندازی میکنیم (از طریق setState یا setter useState).
- react رندر را از بالای درخت آغاز میکند، سپس میبیند که A به عنوان به روزرسانی علامت گذاری نشده است و آن را پشت سر میگذارد.
- react میبیند که B به عنوان نیاز برای به روزرسانی علامت گذاری شده است و آن را رندر میکند. B مانند آخرین بار </ C> را برمیگرداند.
- C در ابتدا به عنوان نیاز به بروزرسانی علامت گذاری نشده. با این حال از آنجا که والد B آن رندر شده است، react اکنون به سمت پایین حرکت کرده و C را نیز رندر میکند.
اکنون احتمالا اکثر کامپوننتها دقیقا مانند آخرین بار نتیجه رندر را برمیگردانند. بنابراین react نیازی به تغییر در درخت واقعی نخواهد داشت. با این حال react همچنان باید از کامپوننتهای سازنده بخواهد خودشان دوباره رندر شوند و خروجی را متفاوت کنند. هر دوی آنها زمان و تلاش زیادی میبرد، به ویژه هنگامی که کامپوننتها بزرگ هستند و محاسبات سنگینی دارند.
بنابراین فهمیدید که رندرهای بیهوده اینگونه اتفاق میافتد.
بهبود عملکرد رندر
طبیعی است که انتظار میرود رندرها بخشی از react باشند. همچنین درست است که اگر خروجی رندر یک کامپوننت تغییر نکرده باشد و این قسمت از درخت نیازی به بروزرسانی نداشته باشد، گاهی اوقات تلاش هدر میرود.
رندر باید همیشه بر اساس Propها و state فعلی کامپوننت باشد. اگر زودتر بدانیم که Propها و State تغییر نمیکنند، خروجی رندر هم تغییر نخواهد کرد. بنابراین میتوانیم با خیال راحت از روند رندر آن کامپوننت صرف نظر کنیم.
وقتی نوبت بهینه سازی میرسد، میتوانید آن را سریعتر اجرا کنید یا حداقل کار کمتری انجام دهید. بیشترین بهینه سازی react مربوط به انجام کمتر کار است.
به یاد داشته باشید قبل از هرگونه بهینه سازی، اندازه گیری کنید تا مرتکب بهینه سازی زودرس نشوید.
react سه API اصلی برای رد شدن از رندر یک کامپوننت پیشنهاد میدهد.
- React.Component.shouldComponentUpdate: چرخه عمر کامپوننت در اوایل فرآیند رندر اتفاق میافتد (البته در به روزرسانی آن). در صورت برگرداندن مقدار false، react از کامپوننت رندر عبور میکند. به طور پیش فرض همیشه مقدار true را برمیگرداند، بنابراین هنگامی که باید از کامپوننت رندر صرف نظر شود، میتوانید منطق خود را اضافه کنید. معمولا وقتی این چرخه را سفارشی میکنیم، propهای قدیمی و state را با موارد جدید مقایسه میکنیم و اگر تغییری ایجاد نشود، مقدار false را برمیگردانیم.
- React.PureComponent: از آنجا که مقایسه propها و state رایج ترین روش برای اجرای shouldComponentUpdate است. PureComponent یک کلاس پایه است که این رفتار را به طور پیش فرض پیاده سازی میکند و میتواند به جای Component + shouldComponentUpdate استفاده شود.
- React.memo: یک کامپوننت با درجه بالاتر داخلی است. این کامپوننت شما را میپذیرد و یک کامپوننت wrapper جدید را برمیگرداند. رفتار پیش فرض کامپوننت wrapper بررسی این است که آیا هرگونه propی تغییر کرده است یا خیر، اگر تغییری نداشته باشد از رندر جلوگیری میکند. همچنین منطق سفارشی شما را برای کار مقایسه قبول میکند، معمولا به جای همه آنها برای مقایسه propهای خاص استفاده میشود.
همه این رویکردها از تکنیک مقایسهای به نام برابری کم عمق استفاده میکنند. این بدان معنی است که فیلد منفرد را در دو شی مختلف بررسی کرده و میبیند آیا محتوای شیها تفاوتی با هم دارد. این روش با === مقایسه را انجام میدهد و روشی ساده و سریع در موتور جاوااسکریپت محسوب میشود.
چگونه ارجاعات جدید Propها بهینه سازیها را از بین میبرند
همانطور که در بالا با تکنیکهای برابری کم عمق آشنا شدیم، بدیهی است که عبور شیهای جدید در مقایسه با شکست مواجه میشود، زیرا "===" ارجاع را مقایسه میکند، حتی اگر محتوای آن تغییر نکرده باشد. این عمل بهینه سازیهای ما را میشکند، کامپوننت هم هنوز رندر میشود و تلاشهای بیشتری را هدر میدهد که از طریق مقایسه propها و درخت متفاوت است. پس مراقب باشید!
در این مثال، ما onClick و دادهها را به عنوان prop به MemoizedChildComponent منتقل میکنیم. اگرچه ChildComponent را بهینه سازی میکنیم، اما این برنامه همچنان هر به روزرسانی ParentComponent را دوباره رندر میکند. زیرا propهای MemoizedChildComponent هر بار شیهای جدیدی به دست میآورند.
انتظار میرود که MemoizedChildComponent رندر را رد کند، زیرا محتوای propهایش یکسان است. بیایید ادامه دهیم و بفهمیم که چگونه میتوانیم این مشکل را برطرف کنیم.
بهینه سازی ارجاعات propها
کامپوننتهای کلاس نگران ایجاد تصادفی ارجاعات شی بازگشتی جدید نیستند، زیرا آنها میتوانند متدهای نمونهای داشته باشند که همیشه همان ارجاع باقی بمانند. با این حال ممکن است لازم باشد برای موارد جداگانه لیست فرزند، پاسخهای منحصر به فردی ایجاد کنند یا مقداری را در یک تابع ناشناس ضبط کرده و آن را به فرزند منتقل کنند که منجر به ایجاد شیهای جدید میشود. react برای بهینه سازی موارد مذکور با هیچگونه ابزار داخلی همراه نیست.
کامپوننت تابع، react دو قلاب useCallback (برای فراخوانی تابع) و useMemo (برای هر نوع دادهای مانند ایجاد اشیا یا محاسبات پیچیده) را پیشنهاد میدهد. در این مقاله میتوانید تفاوت بین این دو هوک را همراه با جزییات مطالعه کنید.
هدف این مقاله این است که مشکل را برطرف کنید، نه آموزش hook. به اعتقاد من منابع زیادی وجود دارد که قلاب را به خوبی توضیح دادهاند. بنابراین در اینجا به جزئیات نمیپردازیم. اما میتوانید مقدمهای سریع بر hook را در اینجا بخوانید.
هر بهینه سازی با هزینه خاص خود همراه است. بهینه سازی با بی دقتی منجر به بدتر شدن عملکرد میشود. همیشه ابتدا با React devtool یا هر ابزار دلخواه خود اندازه گیری کنید، گلوگاه را پیدا کنید و سپس بهینه سازی را انجام دهید.
این کار همیشه سودمند نیست، اگر بود react آن را به عنوان پیش فرض اجرا میکرد، درست است؟
چرا react به طور پیش فرض memo() را در مورد هر کامپوننت قرار نمیدهد؟ این کار سریعتر نیست؟ آیا باید معیاری برای بررسی ایجاد کنیم؟ از خود بپرسید که چرا ()Lodash memoize را برای هر تابع قرار نمیدهید؟ آیا این باعث سریعتر شدن همه توابع نمیشود؟ آیا برای این کار به معیاری نیاز داریم؟ چرا که نه؟
جمع بندی
به طور خلاصه فرآیند رندرینگ در react به دلیل به روزرسانی کامپوننتهای والد، کامپوننت سازنده فرزندان را نیز رندر میکند، این بد نیست زیرا react از این طریق تغییرات را میداند و گاهی اوقات تلاش برای رندر هدر میرود.
صرف نظر کردن از رندر یک روش معمول برای بهینه سازی این موضوع است و این کار به ارجاعات propها مربوط میشود. پس با دقت بهینه سازی کنید و بهینه سازی زودرس انجام ندهید.
ارجاع propها بیشتر از اینها دارای مشکل است. میتوانید این مقاله را در مورد چگونگی تأثیر آن بر وابستگی در استفاده از قلاب مطالعه کنید.
امیدوارم این مقاله برایتان مفید واقع شود. در صورت داشتن هرگونه سوال آن را در بخش زیر حتما با ما در میان بگذارید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید