Memoization و Trade-offهای آن
در سه درس قبل با React.memo، useMemo و useCallback آشنا شدیم؛ هر سه بر پایه یک ایده مشترک بنا شدهاند: ذخیره یک نتیجه قبلی و استفاده دوباره از آن، بهجای محاسبه مجدد. به این ایده، بهطور کلی Memoization گفته میشود. در این درس، این سه ابزار را کنار هم میگذاریم و صادقانه به هزینههایی که با خودشان میآورند نگاه میکنیم.
یک ایده، سه ابزار
| ابزار | چه چیزی را پایدار نگه میدارد | روی چه چیزی اثر میگذارد |
|---|---|---|
| React.memo | خروجی یک کامپوننت | خود آن کامپوننت |
| useMemo | نتیجه یک محاسبه | یک مقدار داخل کامپوننت |
| useCallback | مرجع یک تابع | یک تابع داخل کامپوننت |
هر سه، کار یکسانی انجام میدهند: «اگر ورودی عوض نشده، دوباره محاسبه نکن». تفاوتشان فقط در این است که این ایده را روی چه چیزی پیاده میکنند.
هزینه اول: حافظه
برای اینکه React بتواند تشخیص دهد ورودی عوض شده یا نه، باید مقدار قبلی (Dependency Array قبلی و نتیجه قبلی) را در حافظه نگه دارد. برای یک یا دو Hook memoization، این هزینه ناچیز است؛ اما استفاده افراطی از آن در سراسر یک برنامه بزرگ، حافظه مصرفی را بالا میبرد.
هزینه دوم: مقایسه در هر Render
چه نتیجهای دوباره محاسبه شود چه نشود، React همیشه باید Dependency Array فعلی را با نسخه قبلی مقایسه کند. این مقایسه خودش زمان میبرد؛ برای محاسبات بسیار سبک (مثل جمع دو عدد)، زمان این مقایسه میتواند از خود محاسبه هم بیشتر باشد.
هزینه سوم: پیچیدگی و خوانایی
هر useMemo یا useCallback، یک Dependency Array با خودش میآورد که باید درست نگه داشته شود؛ همانطور که در فصل ششم درباره حلقههای بینهایت و Stale Closure دیدیم، Dependency Array نادرست میتواند باگهای ظریف بسازد. کد پر از memoization، خواندن و ردیابی جریان داده را هم سختتر میکند.
قاعده عملی: memoization پیشفرض نیست
با توجه به این سه هزینه، قاعده درستی که در درس اول این فصل هم اشاره شد این است: memoization را جایی اضافه کنید که یک مشکل واقعی و اندازهگیریشده را حل میکند، نه بهعنوان یک عادت همیشگی روی هر کامپوننت و هر مقدار. کد زیر نمونهای از استفاده بیشازحد و بیفایده است:
function Greeting({ name }) {
// بیفایده: محاسبه بسیار سبک است
const message = useMemo(() => `سلام ${name}`, [name]);
return <p>{message}</p>;
}
یک استثنای مهم: قانون Hookها در ترکیب با شرط
چون طبق Rules of Hooks که در فصل پنجم دیدیم، Hookها باید همیشه به همان ترتیب فراخوانی شوند، نمیتوان useMemo یا useCallback را فقط در برخی شرایط فراخوانی کرد. اگر تصمیم دارید یک محاسبه را memo کنید، این تصمیم باید برای همه Renderها یکسان باشد، نه وابسته به یک شرط زمان اجرا.
راهنمای سریع تصمیمگیری
- محاسبه سبک، بدون ارسال به memo یا Dependency Array دیگر → بدون memoization.
- محاسبه سنگین روی داده بزرگ → useMemo.
- تابعی که به یک کامپوننت memoشده داده میشود یا در Dependency جای دیگری است → useCallback.
- کامپوننتی با زیردرخت سنگین که والدش مکرراً به دلایل بیربط Render میشود → React.memo.
مثال قابل اجرا
در این مثال، یک محاسبه سبک بدون memo و یک محاسبه فرضی سنگین با useMemo کنار هم مقایسه شدهاند.
جمعبندی
React.memo، useMemo و useCallback همگی بر پایه یک ایدهاند: جلوگیری از محاسبه تکراری با ذخیره نتیجه قبلی؛ اما هرکدام هزینه حافظه، هزینه مقایسه و هزینه پیچیدگی کد با خودشان میآورند. بهترین رویکرد، استفاده هدفمند بر اساس یک مشکل واقعی است، نه اعمال خودکار این ابزارها در همهجا. در درس بعدی، به مفهوم Derived Data و حذف محاسبات غیرضروری بدون نیاز به memoization میپردازیم.
