راهنمای جامع برای بهینهسازی عملکرد اپلیکیشنهای React با استفاده از useMemo، useCallback و React.memo. یاد بگیرید چگونه از رندرهای مجدد غیرضروری جلوگیری کرده و تجربه کاربری را بهبود ببخشید.
بهینهسازی عملکرد React: تسلط بر useMemo، useCallback و React.memo
React، یک کتابخانه محبوب جاوا اسکریپت برای ساخت رابطهای کاربری، به خاطر معماری مبتنی بر کامپوننت و سبک اعلانی خود شناخته شده است. با این حال، با افزایش پیچیدگی اپلیکیشنها، عملکرد میتواند به یک نگرانی تبدیل شود. رندرهای مجدد غیرضروری کامپوننتها میتواند منجر به عملکرد کند و تجربه کاربری ضعیف شود. خوشبختانه، React چندین ابزار برای بهینهسازی عملکرد فراهم میکند، از جمله useMemo
، useCallback
و React.memo
. این راهنما به بررسی عمیق این تکنیکها میپردازد و مثالهای عملی و بینشهای کاربردی برای کمک به شما در ساخت اپلیکیشنهای React با کارایی بالا ارائه میدهد.
درک رندرهای مجدد در React
قبل از پرداختن به تکنیکهای بهینهسازی، درک اینکه چرا رندرهای مجدد در React اتفاق میافتند، بسیار مهم است. هنگامی که state یا props یک کامپوننت تغییر میکند، React یک رندر مجدد از آن کامپوننت و به طور بالقوه، کامپوننتهای فرزند آن را آغاز میکند. React از یک DOM مجازی برای بهروزرسانی کارآمد DOM واقعی استفاده میکند، اما رندرهای مجدد بیش از حد همچنان میتوانند بر عملکرد تأثیر بگذارند، به خصوص در اپلیکیشنهای پیچیده. یک پلتفرم تجارت الکترونیک جهانی را تصور کنید که در آن قیمت محصولات به طور مکرر بهروز میشود. بدون بهینهسازی، حتی یک تغییر کوچک در قیمت ممکن است باعث رندرهای مجدد در کل لیست محصولات شود و بر مرور کاربران تأثیر بگذارد.
چرا کامپوننتها دوباره رندر میشوند
- تغییرات State: هنگامی که state یک کامپوننت با استفاده از
useState
یاuseReducer
بهروز میشود، React کامپوننت را دوباره رندر میکند. - تغییرات Prop: اگر یک کامپوننت props جدیدی از کامپوننت والد خود دریافت کند، دوباره رندر خواهد شد.
- رندرهای مجدد والد: هنگامی که یک کامپوننت والد دوباره رندر میشود، کامپوننتهای فرزند آن نیز به طور پیشفرض دوباره رندر میشوند، صرف نظر از اینکه props آنها تغییر کرده باشد یا نه.
- تغییرات Context: کامپوننتهایی که از یک React Context استفاده میکنند، هنگامی که مقدار context تغییر میکند، دوباره رندر میشوند.
هدف از بهینهسازی عملکرد، جلوگیری از رندرهای مجدد غیرضروری است، به طوری که کامپوننتها فقط زمانی بهروز شوند که دادههایشان واقعاً تغییر کرده باشد. سناریویی را در نظر بگیرید که شامل نمایش دادههای لحظهای برای تحلیل بازار سهام است. اگر کامپوننتهای نمودار با هر بهروزرسانی جزئی دادهها به طور غیرضروری دوباره رندر شوند، اپلیکیشن پاسخگو نخواهد بود. بهینهسازی رندرهای مجدد، تجربه کاربری روان و پاسخگو را تضمین میکند.
معرفی useMemo: مموایز کردن محاسبات سنگین
useMemo
یک هوک React است که نتیجه یک محاسبه را مموایز (memoize) میکند. مموایزیشن یک تکنیک بهینهسازی است که نتایج فراخوانیهای توابع سنگین را ذخیره میکند و در صورت تکرار ورودیهای مشابه، از آن نتایج ذخیره شده مجدداً استفاده میکند. این کار از اجرای غیرضروری تابع جلوگیری میکند.
چه زمانی از useMemo استفاده کنیم
- محاسبات سنگین: زمانی که یک کامپوننت نیاز به انجام یک محاسبه سنگین محاسباتی بر اساس props یا state خود دارد.
- برابری ارجاعی (Referential Equality): هنگام ارسال یک مقدار به عنوان prop به یک کامپوننت فرزند که برای تشخیص نیاز به رندر مجدد، به برابری ارجاعی متکی است.
useMemo چگونه کار میکند
useMemo
دو آرگومان میگیرد:
- تابعی که محاسبه را انجام میدهد.
- آرایهای از وابستگیها.
تابع فقط زمانی اجرا میشود که یکی از وابستگیهای موجود در آرایه تغییر کند. در غیر این صورت، useMemo
مقدار مموایز شده قبلی را برمیگرداند.
مثال: محاسبه دنباله فیبوناچی
دنباله فیبوناچی یک مثال کلاسیک از یک محاسبه سنگین است. بیایید یک کامپوننت بسازیم که n-امین عدد فیبوناچی را با استفاده از useMemo
محاسبه میکند.
import React, { useState, useMemo } from 'react';
function Fibonacci({ n }) {
const fibonacciNumber = useMemo(() => {
console.log('Calculating Fibonacci...'); // نشان میدهد که محاسبه چه زمانی اجرا میشود
function calculateFibonacci(num) {
if (num <= 1) {
return num;
}
return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
}
return calculateFibonacci(n);
}, [n]);
return Fibonacci({n}) = {fibonacciNumber}
;
}
function App() {
const [number, setNumber] = useState(5);
return (
setNumber(parseInt(e.target.value))}
/>
);
}
export default App;
در این مثال، تابع calculateFibonacci
فقط زمانی اجرا میشود که prop n
تغییر کند. بدون useMemo
, این تابع در هر بار رندر مجدد کامپوننت Fibonacci
اجرا میشد، حتی اگر n
ثابت باقی میماند. تصور کنید این محاسبه در یک داشبورد مالی جهانی اتفاق میافتد - هر تیک بازار باعث یک محاسبه مجدد کامل میشود و منجر به تأخیر قابل توجهی میگردد. useMemo
از این امر جلوگیری میکند.
معرفی useCallback: مموایز کردن توابع
useCallback
یک هوک دیگر React است که توابع را مموایز میکند. این هوک از ایجاد یک نمونه جدید از تابع در هر بار رندر جلوگیری میکند، که به ویژه هنگام ارسال callbackها به عنوان props به کامپوننتهای فرزند مفید است.
چه زمانی از useCallback استفاده کنیم
- ارسال Callbackها به عنوان Props: هنگام ارسال یک تابع به عنوان prop به یک کامپوننت فرزند که از
React.memo
یاshouldComponentUpdate
برای بهینهسازی رندرهای مجدد استفاده میکند. - مدیریتکنندههای رویداد (Event Handlers): هنگام تعریف توابع مدیریت رویداد در یک کامپوننت برای جلوگیری از رندرهای مجدد غیرضروری کامپوننتهای فرزند.
useCallback چگونه کار میکند
useCallback
دو آرگومان میگیرد:
- تابعی که باید مموایز شود.
- آرایهای از وابستگیها.
تابع فقط زمانی دوباره ایجاد میشود که یکی از وابستگیهای موجود در آرایه تغییر کند. در غیر این صورت، useCallback
همان نمونه تابع قبلی را برمیگرداند.
مثال: مدیریت کلیک روی یک دکمه
بیایید یک کامپوننت با یک دکمه بسازیم که یک تابع callback را فراخوانی میکند. ما از useCallback
برای مموایز کردن تابع callback استفاده خواهیم کرد.
import React, { useState, useCallback } from 'react';
function Button({ onClick, children }) {
console.log('Button re-rendered'); // نشان میدهد که دکمه چه زمانی دوباره رندر میشود
return ;
}
const MemoizedButton = React.memo(Button);
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
setCount((prevCount) => prevCount + 1);
}, []); // آرایه وابستگی خالی به این معنی است که تابع فقط یک بار ایجاد میشود
return (
Count: {count}
Increment
);
}
export default App;
در این مثال، تابع handleClick
فقط یک بار ایجاد میشود زیرا آرایه وابستگی خالی است. هنگامی که کامپوننت App
به دلیل تغییر state count
دوباره رندر میشود، تابع handleClick
ثابت باقی میماند. کامپوننت MemoizedButton
که با React.memo
پوشانده شده است، فقط در صورتی دوباره رندر میشود که props آن تغییر کند. از آنجایی که prop onClick
(یعنی handleClick
) ثابت باقی میماند، کامپوننت Button
به طور غیرضروری دوباره رندر نمیشود. یک اپلیکیشن نقشه تعاملی را تصور کنید. هر بار که کاربر تعامل میکند، دهها کامپوننت دکمه ممکن است تحت تأثیر قرار گیرند. بدون useCallback
، این دکمهها به طور غیرضروری دوباره رندر میشوند و تجربهای کند ایجاد میکنند. استفاده از useCallback
تعامل روانتری را تضمین میکند.
معرفی React.memo: مموایز کردن کامپوننتها
React.memo
یک کامپوننت مرتبه بالاتر (HOC) است که یک کامپوننت تابعی را مموایز میکند. این کار از رندر مجدد کامپوننت در صورتی که props آن تغییر نکرده باشد، جلوگیری میکند. این شبیه به PureComponent
برای کامپوننتهای کلاسی است.
چه زمانی از React.memo استفاده کنیم
- کامپوننتهای خالص (Pure Components): زمانی که خروجی یک کامپوننت فقط به props آن بستگی دارد و state داخلی ندارد.
- رندرینگ سنگین: زمانی که فرآیند رندرینگ یک کامپوننت از نظر محاسباتی سنگین است.
- رندرهای مجدد مکرر: زمانی که یک کامپوننت به طور مکرر دوباره رندر میشود در حالی که props آن تغییر نکرده است.
React.memo چگونه کار میکند
React.memo
یک کامپوننت تابعی را در بر میگیرد و props قبلی و بعدی را به صورت سطحی (shallowly) مقایسه میکند. اگر propsها یکسان باشند، کامپوننت دوباره رندر نخواهد شد.
مثال: نمایش پروفایل کاربر
بیایید یک کامپوننت بسازیم که پروفایل یک کاربر را نمایش میدهد. ما از React.memo
برای جلوگیری از رندرهای مجدد غیرضروری در صورتی که دادههای کاربر تغییر نکرده باشد، استفاده خواهیم کرد.
import React from 'react';
function UserProfile({ user }) {
console.log('UserProfile re-rendered'); // نشان میدهد که کامپوننت چه زمانی دوباره رندر میشود
return (
Name: {user.name}
Email: {user.email}
);
}
const MemoizedUserProfile = React.memo(UserProfile, (prevProps, nextProps) => {
// تابع مقایسه سفارشی (اختیاری)
return prevProps.user.id === nextProps.user.id; // فقط در صورتی دوباره رندر کن که شناسه کاربر تغییر کند
});
function App() {
const [user, setUser] = React.useState({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
});
const updateUser = () => {
setUser({ ...user, name: 'Jane Doe' }); // تغییر نام
};
return (
);
}
export default App;
در این مثال، کامپوننت MemoizedUserProfile
فقط در صورتی دوباره رندر میشود که prop user.id
تغییر کند. حتی اگر سایر ویژگیهای شیء user
تغییر کنند (مانند نام یا ایمیل)، کامپوننت دوباره رندر نخواهد شد مگر اینکه شناسه متفاوت باشد. این تابع مقایسه سفارشی در `React.memo` امکان کنترل دقیق بر زمان رندر مجدد کامپوننت را فراهم میکند. یک پلتفرم رسانه اجتماعی با پروفایلهای کاربری که دائماً در حال بهروزرسانی هستند را در نظر بگیرید. بدون `React.memo`، تغییر وضعیت یا عکس پروفایل کاربر باعث رندر مجدد کامل کامپوننت پروفایل میشود، حتی اگر جزئیات اصلی کاربر ثابت باقی بماند. `React.memo` امکان بهروزرسانیهای هدفمند را فراهم کرده و عملکرد را به طور قابل توجهی بهبود میبخشد.
ترکیب useMemo، useCallback و React.memo
این سه تکنیک زمانی بیشترین تأثیر را دارند که با هم استفاده شوند. useMemo
محاسبات سنگین را مموایز میکند، useCallback
توابع را مموایز میکند و React.memo
کامپوننتها را مموایز میکند. با ترکیب این تکنیکها، میتوانید تعداد رندرهای مجدد غیرضروری را در اپلیکیشن React خود به طور قابل توجهی کاهش دهید.
مثال: یک کامپوننت پیچیده
بیایید یک کامپوننت پیچیدهتر بسازیم که نحوه ترکیب این تکنیکها را نشان دهد.
import React, { useState, useCallback, useMemo } from 'react';
function ListItem({ item, onUpdate, onDelete }) {
console.log(`ListItem ${item.id} re-rendered`); // نشان میدهد که کامپوننت چه زمانی دوباره رندر میشود
return (
{item.text}
);
}
const MemoizedListItem = React.memo(ListItem);
function List({ items, onUpdate, onDelete }) {
console.log('List re-rendered'); // نشان میدهد که کامپوننت چه زمانی دوباره رندر میشود
return (
{items.map((item) => (
))}
);
}
const MemoizedList = React.memo(List);
function App() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const handleUpdate = useCallback((id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, text: `Updated ${item.text}` } : item
)
);
}, []);
const handleDelete = useCallback((id) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
}, []);
const memoizedItems = useMemo(() => items, [items]);
return (
);
}
export default App;
در این مثال:
useCallback
برای مموایز کردن توابعhandleUpdate
وhandleDelete
استفاده میشود و از ایجاد مجدد آنها در هر بار رندر جلوگیری میکند.useMemo
برای مموایز کردن آرایهitems
استفاده میشود و از رندر مجدد کامپوننتList
در صورتی که ارجاع آرایه تغییر نکرده باشد، جلوگیری میکند.React.memo
برای مموایز کردن کامپوننتهایListItem
وList
استفاده میشود و از رندر مجدد آنها در صورتی که props آنها تغییر نکرده باشد، جلوگیری میکند.
این ترکیب از تکنیکها تضمین میکند که کامپوننتها فقط در صورت لزوم دوباره رندر میشوند و منجر به بهبود قابل توجه عملکرد میشود. یک ابزار مدیریت پروژه در مقیاس بزرگ را تصور کنید که در آن لیستهای وظایف دائماً در حال بهروزرسانی، حذف و مرتبسازی مجدد هستند. بدون این بهینهسازیها، هر تغییر کوچکی در لیست وظایف باعث ایجاد یک زنجیره از رندرهای مجدد میشود و اپلیکیشن را کند و غیرپاسخگو میکند. با استفاده استراتژیک از useMemo
، useCallback
و React.memo
، اپلیکیشن میتواند حتی با دادههای پیچیده و بهروزرسانیهای مکرر، کارآمد باقی بماند.
تکنیکهای بهینهسازی اضافی
در حالی که useMemo
، useCallback
و React.memo
ابزارهای قدرتمندی هستند، تنها گزینهها برای بهینهسازی عملکرد React نیستند. در اینجا چند تکنیک اضافی برای در نظر گرفتن وجود دارد:
- تقسیم کد (Code Splitting): اپلیکیشن خود را به قطعات کوچکتری تقسیم کنید که میتوانند در صورت تقاضا بارگذاری شوند. این کار زمان بارگذاری اولیه را کاهش میدهد و عملکرد کلی را بهبود میبخشد.
- بارگذاری تنبل (Lazy Loading): کامپوننتها و منابع را فقط زمانی که به آنها نیاز است بارگذاری کنید. این میتواند به ویژه برای تصاویر و سایر داراییهای بزرگ مفید باشد.
- مجازیسازی (Virtualization): فقط بخش قابل مشاهده از یک لیست یا جدول بزرگ را رندر کنید. این میتواند هنگام کار با مجموعه دادههای بزرگ، عملکرد را به طور قابل توجهی بهبود بخشد. کتابخانههایی مانند
react-window
وreact-virtualized
میتوانند در این زمینه کمک کنند. - Debouncing و Throttling: نرخ اجرای توابع را محدود کنید. این میتواند برای مدیریت رویدادهایی مانند اسکرول و تغییر اندازه مفید باشد.
- تغییرناپذیری (Immutability): از ساختارهای داده تغییرناپذیر برای جلوگیری از تغییرات تصادفی و سادهسازی تشخیص تغییرات استفاده کنید.
ملاحظات جهانی برای بهینهسازی
هنگام بهینهسازی اپلیکیشنهای React برای مخاطبان جهانی، مهم است که عواملی مانند تأخیر شبکه، قابلیتهای دستگاه و بومیسازی را در نظر بگیرید. در اینجا چند نکته وجود دارد:
- شبکههای تحویل محتوا (CDNs): از یک CDN برای ارائه داراییهای ثابت از مکانهای نزدیکتر به کاربران خود استفاده کنید. این کار تأخیر شبکه را کاهش داده و زمان بارگذاری را بهبود میبخشد.
- بهینهسازی تصاویر: تصاویر را برای اندازهها و وضوحهای مختلف صفحه بهینه کنید. از تکنیکهای فشردهسازی برای کاهش اندازه فایلها استفاده کنید.
- بومیسازی (Localization): فقط منابع زبانی لازم برای هر کاربر را بارگذاری کنید. این کار زمان بارگذاری اولیه را کاهش میدهد و تجربه کاربری را بهبود میبخشد.
- بارگذاری تطبیقی (Adaptive Loading): اتصال شبکه و قابلیتهای دستگاه کاربر را تشخیص دهید و رفتار اپلیکیشن را بر اساس آن تنظیم کنید. به عنوان مثال، ممکن است انیمیشنها را غیرفعال کنید یا کیفیت تصویر را برای کاربران با اتصالات شبکه کند یا دستگاههای قدیمیتر کاهش دهید.
نتیجهگیری
بهینهسازی عملکرد اپلیکیشن React برای ارائه یک تجربه کاربری روان و پاسخگو بسیار مهم است. با تسلط بر تکنیکهایی مانند useMemo
، useCallback
و React.memo
و با در نظر گرفتن استراتژیهای بهینهسازی جهانی، میتوانید اپلیکیشنهای React با کارایی بالا بسازید که برای پاسخگویی به نیازهای یک پایگاه کاربری متنوع مقیاسپذیر باشند. به یاد داشته باشید که اپلیکیشن خود را برای شناسایی گلوگاههای عملکرد پروفایل کنید و این تکنیکهای بهینهسازی را به صورت استراتژیک به کار ببرید. بهینهسازی زودهنگام انجام ندهید – بر روی مناطقی تمرکز کنید که میتوانید بیشترین تأثیر را داشته باشید.
این راهنما یک پایه محکم برای درک و پیادهسازی بهینهسازیهای عملکرد React فراهم میکند. همانطور که به توسعه اپلیکیشنهای React ادامه میدهید، به یاد داشته باشید که عملکرد را در اولویت قرار دهید و به طور مداوم به دنبال راههای جدید برای بهبود تجربه کاربری باشید.