راهنمای جامع برای بهینهسازی اپلیکیشنهای React با جلوگیری از رندرهای مجدد غیرضروری. تکنیکهایی مانند memoization، PureComponent، shouldComponentUpdate و موارد دیگر را برای بهبود عملکرد بیاموزید.
بهینهسازی رندر در React: تسلط بر جلوگیری از رندرهای مجدد غیرضروری
ریاکت، یک کتابخانه قدرتمند جاوااسکریپت برای ساخت رابطهای کاربری، گاهی اوقات ممکن است به دلیل رندرهای مجدد بیش از حد یا غیرضروری، با مشکلات عملکردی مواجه شود. در اپلیکیشنهای پیچیده با کامپوننتهای زیاد، این رندرهای مجدد میتوانند به طور قابل توجهی عملکرد را کاهش داده و منجر به تجربه کاربری کندی شوند. این راهنما یک مرور جامع از تکنیکهای جلوگیری از رندرهای مجدد غیرضروری در React ارائه میدهد تا اطمینان حاصل شود که اپلیکیشنهای شما برای کاربران در سراسر جهان سریع، کارآمد و پاسخگو هستند.
درک رندرهای مجدد در React
قبل از پرداختن به تکنیکهای بهینهسازی، درک نحوه کار فرآیند رندرینگ در React بسیار مهم است. هنگامی که state یا props یک کامپوننت تغییر میکند، React یک رندر مجدد برای آن کامپوننت و فرزندانش را آغاز میکند. این فرآیند شامل بهروزرسانی DOM مجازی و مقایسه آن با نسخه قبلی برای تعیین حداقل مجموعه تغییراتی است که باید در DOM واقعی اعمال شود.
با این حال، همه تغییرات state یا prop نیازی به بهروزرسانی DOM ندارند. اگر DOM مجازی جدید با نسخه قبلی یکسان باشد، رندر مجدد اساساً هدر دادن منابع است. این رندرهای مجدد غیرضروری چرخههای ارزشمند CPU را مصرف میکنند و میتوانند منجر به مشکلات عملکردی شوند، به خصوص در اپلیکیشنهایی با درخت کامپوننتهای پیچیده.
شناسایی رندرهای مجدد غیرضروری
اولین قدم در بهینهسازی رندرها، شناسایی محل وقوع آنهاست. React چندین ابزار برای کمک به شما در این زمینه فراهم میکند:
۱. پروفایلر React
پروفایلر React که در افزونه React DevTools برای کروم و فایرفاکس موجود است، به شما امکان میدهد عملکرد کامپوننتهای React خود را ضبط و تحلیل کنید. این ابزار اطلاعاتی در مورد اینکه کدام کامپوننتها در حال رندر مجدد هستند، چه مدت زمانی برای رندر شدن صرف میکنند و چرا در حال رندر مجدد هستند، ارائه میدهد.
برای استفاده از پروفایلر، کافیست دکمه "Record" را در DevTools فعال کرده و با اپلیکیشن خود تعامل کنید. پس از ضبط، پروفایلر یک نمودار شعلهای (flame chart) نمایش میدهد که درخت کامپوننت و زمانهای رندر آن را به تصویر میکشد. کامپوننتهایی که زمان زیادی برای رندر شدن صرف میکنند یا به طور مکرر رندر مجدد میشوند، کاندیداهای اصلی برای بهینهسازی هستند.
۲. کتابخانه Why Did You Render?
"Why Did You Render?" کتابخانهای است که React را پچ میکند تا با لاگ کردن props خاصی که باعث رندر مجدد شدهاند، شما را از رندرهای مجدد بالقوه غیرضروری مطلع سازد. این میتواند در شناسایی علت اصلی مشکلات رندر مجدد بسیار مفید باشد.
برای استفاده از "Why Did You Render?"، آن را به عنوان یک وابستگی توسعه (development dependency) نصب کنید:
npm install @welldone-software/why-did-you-render --save-dev
سپس، آن را در نقطه ورود اپلیکیشن خود (مانند index.js) وارد کنید:
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, {
include: [/.*/]
});
}
این کد "Why Did You Render?" را در حالت توسعه فعال کرده و اطلاعاتی در مورد رندرهای مجدد بالقوه غیرضروری را در کنسول لاگ میکند.
۳. دستورات Console.log
یک تکنیک ساده اما مؤثر، اضافه کردن دستورات console.log
در متد render
کامپوننت شما (یا بدنه کامپوننت تابعی) برای ردیابی زمان رندر مجدد آن است. اگرچه این روش نسبت به پروفایلر یا "Why Did You Render?" کمتر پیچیده است، اما میتواند به سرعت کامپوننتهایی را که بیش از حد انتظار رندر مجدد میشوند، مشخص کند.
تکنیکهایی برای جلوگیری از رندرهای مجدد غیرضروری
پس از شناسایی کامپوننتهایی که باعث مشکلات عملکردی میشوند، میتوانید از تکنیکهای مختلفی برای جلوگیری از رندرهای مجدد غیرضروری استفاده کنید:
۱. مموایزیشن (Memoization)
مموایزیشن یک تکنیک بهینهسازی قدرتمند است که شامل کش کردن نتایج فراخوانیهای توابع پرهزینه و بازگرداندن نتیجه کششده در صورت تکرار ورودیهای یکسان است. در React، از مموایزیشن میتوان برای جلوگیری از رندر مجدد کامپوننتها در صورتی که props آنها تغییر نکرده باشد، استفاده کرد.
الف. React.memo
React.memo
یک کامپوننت مرتبه بالاتر (higher-order component) است که یک کامپوننت تابعی را مموایز میکند. این تابع به صورت سطحی (shallowly) props فعلی را با props قبلی مقایسه کرده و تنها در صورتی که props تغییر کرده باشد، کامپوننت را مجدداً رندر میکند.
مثال:
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.data}</div>;
});
به طور پیشفرض، React.memo
یک مقایسه سطحی روی تمام props انجام میدهد. شما میتوانید یک تابع مقایسه سفارشی را به عنوان آرگومان دوم به React.memo
ارسال کنید تا منطق مقایسه را سفارشیسازی کنید.
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.data}</div>;
}, (prevProps, nextProps) => {
// اگر propsها مساوی باشند true و در غیر این صورت false برگردانید
return prevProps.data === nextProps.data;
});
ب. useMemo
useMemo
یک هوک React است که نتیجه یک محاسبه را مموایز میکند. این هوک یک تابع و یک آرایه از وابستگیها را به عنوان آرگومان میگیرد. تابع تنها زمانی دوباره اجرا میشود که یکی از وابستگیها تغییر کند و نتیجه مموایزشده در رندرهای بعدی بازگردانده میشود.
useMemo
به ویژه برای مموایز کردن محاسبات پرهزینه یا ایجاد ارجاعات پایدار به اشیاء یا توابعی که به عنوان props به کامپوننتهای فرزند ارسال میشوند، مفید است.
مثال:
const memoizedValue = useMemo(() => {
// یک محاسبه پرهزینه را اینجا انجام دهید
return computeExpensiveValue(a, b);
}, [a, b]);
۲. PureComponent
PureComponent
یک کلاس پایه برای کامپوننتهای React است که یک مقایسه سطحی از props و state را در متد shouldComponentUpdate
خود پیادهسازی میکند. اگر props و state تغییر نکرده باشند، کامپوننت مجدداً رندر نخواهد شد.
PureComponent
انتخاب خوبی برای کامپوننتهایی است که برای رندر شدن صرفاً به props و state خود وابسته هستند و به context یا عوامل خارجی دیگر تکیه ندارند.
مثال:
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.data}</div>;
}
}
نکته مهم: PureComponent
و React.memo
مقایسههای سطحی انجام میدهند. این بدان معناست که آنها فقط ارجاعات اشیاء و آرایهها را مقایسه میکنند، نه محتویات آنها را. اگر props یا state شما شامل اشیاء یا آرایههای تو در تو باشد، ممکن است نیاز به استفاده از تکنیکهایی مانند تغییرناپذیری (immutability) داشته باشید تا اطمینان حاصل شود که تغییرات به درستی شناسایی میشوند.
۳. shouldComponentUpdate
متد چرخه حیات shouldComponentUpdate
به شما امکان میدهد به صورت دستی کنترل کنید که آیا یک کامپوننت باید مجدداً رندر شود یا خیر. این متد props بعدی و state بعدی را به عنوان آرگومان دریافت میکند و باید true
را در صورت نیاز به رندر مجدد و false
را در غیر این صورت برگرداند.
در حالی که shouldComponentUpdate
بیشترین کنترل را بر رندر مجدد فراهم میکند، بیشترین تلاش دستی را نیز میطلبد. شما باید props و state مربوطه را با دقت مقایسه کنید تا مشخص شود آیا رندر مجدد ضروری است یا خیر.
مثال:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// props و state را اینجا مقایسه کنید
return nextProps.data !== this.props.data || nextState.count !== this.state.count;
}
render() {
return <div>{this.props.data}</div>;
}
}
احتیاط: پیادهسازی نادرست shouldComponentUpdate
میتواند منجر به رفتار غیرمنتظره و باگ شود. اطمینان حاصل کنید که منطق مقایسه شما کامل است و تمام عوامل مربوطه را در نظر میگیرد.
۴. useCallback
useCallback
یک هوک React است که تعریف یک تابع را مموایز میکند. این هوک یک تابع و یک آرایه از وابستگیها را به عنوان آرگومان میگیرد. تابع تنها زمانی دوباره تعریف میشود که یکی از وابستگیها تغییر کند و تابع مموایزشده در رندرهای بعدی بازگردانده میشود.
useCallback
به ویژه برای ارسال توابع به عنوان props به کامپوننتهای فرزندی که از React.memo
یا PureComponent
استفاده میکنند، مفید است. با مموایز کردن تابع، میتوانید از رندر مجدد غیرضروری کامپوننت فرزند هنگام رندر مجدد کامپوننت والد جلوگیری کنید.
مثال:
const handleClick = useCallback(() => {
// رویداد کلیک را مدیریت کنید
console.log('Clicked!');
}, []);
۵. تغییرناپذیری (Immutability)
تغییرناپذیری یک مفهوم برنامهنویسی است که شامل رفتار با دادهها به عنوان دادههای تغییرناپذیر است، به این معنی که پس از ایجاد، نمیتوان آنها را تغییر داد. هنگام کار با دادههای تغییرناپذیر، هرگونه تغییری منجر به ایجاد یک ساختار داده جدید میشود به جای اینکه ساختار موجود را تغییر دهد.
تغییرناپذیری برای بهینهسازی رندرهای مجدد React بسیار مهم است زیرا به React اجازه میدهد تا به راحتی تغییرات در props و state را با استفاده از مقایسههای سطحی تشخیص دهد. اگر یک شیء یا آرایه را مستقیماً تغییر دهید، React قادر به تشخیص تغییر نخواهد بود زیرا ارجاع به آن شیء یا آرایه ثابت باقی میماند.
شما میتوانید از کتابخانههایی مانند Immutable.js یا Immer برای کار با دادههای تغییرناپذیر در React استفاده کنید. این کتابخانهها ساختارهای داده و توابعی را فراهم میکنند که ایجاد و دستکاری دادههای تغییرناپذیر را آسانتر میکنند.
مثال با استفاده از Immer:
import { useImmer } from 'use-immer';
function MyComponent() {
const [data, setData] = useImmer({
name: 'John',
age: 30
});
const updateName = () => {
setData(draft => {
draft.name = 'Jane';
});
};
return (
<div>
<p>Name: {data.name}</p>
<button onClick={updateName}>Update Name</button>
</div>
);
}
۶. تقسیم کد (Code Splitting) و بارگذاری تنبل (Lazy Loading)
تقسیم کد تکنیکی است که شامل تقسیم کد اپلیکیشن شما به قطعات کوچکتر است که میتوانند بر اساس تقاضا بارگذاری شوند. این میتواند به طور قابل توجهی زمان بارگذاری اولیه اپلیکیشن شما را بهبود بخشد، زیرا مرورگر فقط کدی را که برای نمای فعلی ضروری است، دانلود میکند.
React پشتیبانی داخلی برای تقسیم کد با استفاده از تابع React.lazy
و کامپوننت Suspense
فراهم میکند. React.lazy
به شما امکان میدهد کامپوننتها را به صورت پویا وارد کنید، در حالی که Suspense
به شما امکان میدهد یک UI جایگزین (fallback) را هنگام بارگذاری کامپوننت نمایش دهید.
مثال:
import React, { Suspense } from 'react';
const MyComponent = React.lazy(() => import('./MyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}
۷. استفاده کارآمد از کلیدها (Keys)
هنگام رندر کردن لیستهایی از عناصر در React، ارائه کلیدهای منحصر به فرد به هر عنصر بسیار مهم است. کلیدها به React کمک میکنند تا تشخیص دهد کدام عناصر تغییر کرده، اضافه شده یا حذف شدهاند، و به آن امکان میدهد DOM را به طور کارآمد بهروزرسانی کند.
از استفاده از ایندکسهای آرایه به عنوان کلید خودداری کنید، زیرا با تغییر ترتیب عناصر در آرایه، ایندکسها نیز میتوانند تغییر کنند و منجر به رندرهای مجدد غیرضروری شوند. به جای آن، از یک شناسه منحصر به فرد برای هر عنصر استفاده کنید، مانند یک ID از پایگاه داده یا یک UUID تولید شده.
۸. بهینهسازی استفاده از Context
React Context راهی برای به اشتراک گذاشتن دادهها بین کامپوننتها بدون نیاز به پاس دادن صریح props در هر سطح از درخت کامپوننت فراهم میکند. با این حال، استفاده بیش از حد از Context میتواند منجر به مشکلات عملکردی شود، زیرا هر کامپوننتی که از یک Context استفاده میکند، هر بار که مقدار Context تغییر کند، مجدداً رندر میشود.
برای بهینهسازی استفاده از Context، این استراتژیها را در نظر بگیرید:
- استفاده از چندین Context کوچکتر: به جای استفاده از یک Context بزرگ برای ذخیره تمام دادههای اپلیکیشن، آن را به Contextهای کوچکتر و متمرکزتر تقسیم کنید. این کار تعداد کامپوننتهایی را که با تغییر یک مقدار خاص Context مجدداً رندر میشوند، کاهش میدهد.
- مموایز کردن مقادیر Context: از
useMemo
برای مموایز کردن مقادیری که توسط provider کانتکست ارائه میشوند، استفاده کنید. این کار از رندرهای مجدد غیرضروری مصرفکنندگان Context در صورتی که مقادیر واقعاً تغییر نکرده باشند، جلوگیری میکند. - در نظر گرفتن جایگزینهایی برای Context: در برخی موارد، راهحلهای دیگر مدیریت state مانند Redux یا Zustand ممکن است مناسبتر از Context باشند، به خصوص برای اپلیکیشنهای پیچیده با تعداد زیادی کامپوننت و بهروزرسانیهای مکرر state.
ملاحظات بینالمللی
هنگام بهینهسازی اپلیکیشنهای React برای مخاطبان جهانی، در نظر گرفتن عوامل زیر مهم است:
- سرعتهای مختلف شبکه: کاربران در مناطق مختلف ممکن است سرعتهای شبکه بسیار متفاوتی داشته باشند. اپلیکیشن خود را به گونهای بهینه کنید که میزان دادهای که باید دانلود و از طریق شبکه منتقل شود به حداقل برسد. استفاده از تکنیکهایی مانند بهینهسازی تصاویر، تقسیم کد و بارگذاری تنبل را در نظر بگیرید.
- قابلیتهای دستگاه: کاربران ممکن است با دستگاههای متنوعی، از گوشیهای هوشمند پیشرفته گرفته تا دستگاههای قدیمیتر و کمقدرتتر، به اپلیکیشن شما دسترسی داشته باشند. اپلیکیشن خود را برای عملکرد خوب در طیف وسیعی از دستگاهها بهینه کنید. استفاده از تکنیکهایی مانند طراحی واکنشگرا، تصاویر تطبیقی و پروفایلگیری عملکرد را در نظر بگیرید.
- بومیسازی: اگر اپلیکیشن شما برای چندین زبان بومیسازی شده است، اطمینان حاصل کنید که فرآیند بومیسازی مشکلات عملکردی ایجاد نکند. از کتابخانههای بومیسازی کارآمد استفاده کنید و از هاردکد کردن رشتههای متنی به طور مستقیم در کامپوننتهای خود خودداری کنید.
مثالهای واقعی
بیایید چند مثال واقعی از نحوه اعمال این تکنیکهای بهینهسازی را بررسی کنیم:
۱. لیست محصولات در یک فروشگاه اینترنتی
یک وبسایت تجارت الکترونیک را با یک صفحه لیست محصولات که صدها محصول را نمایش میدهد، تصور کنید. هر آیتم محصول به عنوان یک کامپوننت جداگانه رندر میشود.
بدون بهینهسازی، هر بار که کاربر لیست محصولات را فیلتر یا مرتب میکند، تمام کامپوننتهای محصول مجدداً رندر میشوند که منجر به تجربهای کند و ناخوشایند میشود. برای بهینهسازی این مورد، میتوانید از React.memo
برای مموایز کردن کامپوننتهای محصول استفاده کنید تا اطمینان حاصل شود که آنها فقط زمانی که props آنها (مانند نام محصول، قیمت، تصویر) تغییر میکند، مجدداً رندر میشوند.
۲. فید شبکههای اجتماعی
یک فید شبکههای اجتماعی معمولاً لیستی از پستها را نمایش میدهد که هر کدام دارای نظرات، لایکها و سایر عناصر تعاملی هستند. رندر مجدد کل فید هر بار که کاربر پستی را لایک میکند یا نظری اضافه میکند، ناکارآمد خواهد بود.
برای بهینهسازی این مورد، میتوانید از useCallback
برای مموایز کردن کنترلکنندههای رویداد برای لایک کردن و نظر دادن روی پستها استفاده کنید. این کار از رندر مجدد غیرضروری کامپوننتهای پست هنگام فعال شدن این کنترلکنندههای رویداد جلوگیری میکند.
۳. داشبورد مصورسازی دادهها
یک داشبورد مصورسازی دادهها اغلب نمودارها و گرافهای پیچیدهای را نمایش میدهد که به طور مکرر با دادههای جدید بهروز میشوند. رندر مجدد این نمودارها هر بار که دادهها تغییر میکنند، میتواند از نظر محاسباتی پرهزینه باشد.
برای بهینهسازی این مورد، میتوانید از useMemo
برای مموایز کردن دادههای نمودار استفاده کنید و نمودارها را تنها زمانی که دادههای مموایزشده تغییر میکنند، مجدداً رندر کنید. این کار به طور قابل توجهی تعداد رندرهای مجدد را کاهش داده و عملکرد کلی داشبورد را بهبود میبخشد.
بهترین شیوهها
در اینجا چند بهترین شیوه برای به خاطر سپردن هنگام بهینهسازی رندرهای مجدد React آورده شده است:
- اپلیکیشن خود را پروفایل کنید: از پروفایلر React یا "Why Did You Render?" برای شناسایی کامپوننتهایی که باعث مشکلات عملکردی میشوند، استفاده کنید.
- با موارد ساده شروع کنید: بر روی بهینهسازی کامپوننتهایی که بیشترین تکرار رندر مجدد را دارند یا بیشترین زمان را برای رندر شدن صرف میکنند، تمرکز کنید.
- از مموایزیشن با احتیاط استفاده کنید: هر کامپوننتی را مموایز نکنید، زیرا خود مموایزیشن هزینهای دارد. فقط کامپوننتهایی را که واقعاً باعث مشکلات عملکردی میشوند، مموایز کنید.
- از تغییرناپذیری استفاده کنید: از ساختارهای داده تغییرناپذیر استفاده کنید تا تشخیص تغییرات در props و state برای React آسانتر شود.
- کامپوننتها را کوچک و متمرکز نگه دارید: کامپوننتهای کوچکتر و متمرکزتر برای بهینهسازی و نگهداری آسانتر هستند.
- بهینهسازیهای خود را تست کنید: پس از اعمال تکنیکهای بهینهسازی، اپلیکیشن خود را به طور کامل تست کنید تا اطمینان حاصل شود که بهینهسازیها تأثیر مطلوب را داشته و هیچ باگ جدیدی ایجاد نکردهاند.
نتیجهگیری
جلوگیری از رندرهای مجدد غیرضروری برای بهینهسازی عملکرد اپلیکیشنهای React بسیار مهم است. با درک نحوه کار فرآیند رندرینگ React و به کارگیری تکنیکهای شرح داده شده در این راهنما، میتوانید به طور قابل توجهی پاسخگویی و کارایی اپلیکیشنهای خود را بهبود بخشیده و تجربه کاربری بهتری را برای کاربران در سراسر جهان فراهم کنید. به یاد داشته باشید که اپلیکیشن خود را پروفایل کنید، کامپوننتهایی را که باعث مشکلات عملکردی میشوند شناسایی کنید و تکنیکهای بهینهسازی مناسب را برای رفع آن مشکلات به کار بگیرید. با پیروی از این بهترین شیوهها، میتوانید اطمینان حاصل کنید که اپلیکیشنهای React شما صرف نظر از پیچیدگی یا اندازه کدبیس شما، سریع، کارآمد و مقیاسپذیر هستند.