پرش به مطلب اصلی

rendering optimization

طرز تهیه یک اپ ری اکت بهینه

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

اصل Single source of truth

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

function Input() {
[value, setValue] = useState('');
[isTouched, setIsTouched] = useState(false);
return (
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
setIsTouched(true);
}}
/>
);
}

این کامپوننت برای بررسی دست خورده بودن ورودی و نشان دادن ارور از isTouched و برای نگهداری مقدار تایپ شده از value استفاده می کنه. اما لحظه ای که اولین کاراکتر تایپ میشه هر دو state تغییر می کنند و دو رندر متوالی داریم.

در این حالت میشه از یک استیت جدید به عنوان single source of truth استفاده کرد و هر دو state رو در اون قرار داد تا جلوی رندر متوالی گرفته بشه.

function Input() {
[input, setInput] = useState({value: '', isTouched: false});
return (
<input
value={input.value}
onChange={(e) => {
setInput({value: e.target.value, isTouched: true});
}}
/>
);
}

راه حل قویتر استفاده از useReducer هست که شاید در این مثال کمی اضافی به نظر برسه ولی بهمون اطمینان میده که در صورت اضافه شدن state های جدید به کامپوننت هم مشکلی پیش نمیاد چون میشه به جای تصور مستقیم state ها، نتایج یک عمل مشخص رو تصور کنیم و تغییرات لازم رو اعمال کنیم.

function Input() {
const [input, dispatch] = useReducer(
(state, action) => {
switch (action.type) {
case 'SET_VALUE':
return {...state, value: action.payload, isTouched: true};
default:
return state;
}
},
{value: '', isTouched: false}
);
return (
<input
value={input.value}
onChange={(e) => {
dispatch({type: 'SET_VALUE', payload: e.target.value});
}}
/>
);
}
نکته

استفاده از useReducer همیشه خوب نیست. در این مورد میتونید بخش مربوط به این انتخاب رو تو داک ری اکت بخونید.

نساختن یک کامپوننت در بدنه کامپوننت دیگر

این کار باعث re-mount علاوه بر ری رندر می شود. در واقع ری اکت این کامپوننت را حذف و دوباره از صفر ساخته و در جای قبلی قرار می دهد که بسیار کند تر از ری رندر است.

نکته

این کار باعث می شود lifecycle کامپوننت نیز بهم ریخته و مثلا useEffect دوباره اجرا شود یا برای لحظه ای جای کامپوننت در DOM سفید یا خالی شود

پایینتر بردن state و کامپوننت ساختن دور استیت های بدون ارتباط

این الگو تلاش می کند هر state پس از تغییر کمترین بخش ممکن را ری رندر کند. به طور مثال اگر استیت یک checkbox در کامپوننت پدر که کل فرم را مدیریت می کند ذخیره شود، کل فرم وقتی تیک checkbox را بزنیم ری رندر می شود. اما اگر استیت checkbox در کامپوننت خود checkbox ذخیره شود، فقط checkbox ری رندر می شود.

استفاده از props به جای رندر مستقیم

این الگو نیز مانند قبلی به جدا کردن state ها اشاره می کند.
اگر یک المنت به عنوان یک پراپ یا children که خود نیز مانند یک پراپ معمولی است دریافت شود، ری رندر تاثیری بر روی این کامپوننت نخواهد داشت

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

استفاده از این دو هوک یکی از روش های اصلی بهینه سازی رندر هاست که در این مورد با جزئیات در بخش useCallback & useMemo صحبت شده.
جزئیات مربوط به استفاده از آنها با React.memo در ادامه بررسی می شوند

React.memo

این تابع تلاش می کنه تا وقتی پراپ های یک کامپوننت تغییر نکرده باشند، ری رندری اتفاق نیوفته. در واقع شبیه useMemo عمل میکنه ولی برای کامپوننت کامل.

برای اینکه React.memo درست کار کنه و ری رندری اتفاق نیوفته علاوه بر خود کامپوننت، تمام پراپ های غیر primitive هم باید با useMemo یا روش دیگه ای memoize شن

دلیل این کار اینه که رفرنس پراپ های غیر پریمیتیو بعد از ری رندر پدر تغییر میکنه، و همین تغییر برای ری رندر کافیه. در نتیجه React.memo میتونه اشتباهی ری رندر کنه