React.memo: جلوگیری از Re-render غیرضروری یک کامپوننت

در درس قبل دیدیم یکی از دلایل رایج Re-render اضافه، رندر شدن کامپوننت والد است که به‌طور پیش‌فرض، کل زیردرخت را هم دوباره Render می‌کند؛ حتی فرزندانی که Props‌شان عوض نشده. React.memo اولین ابزاری است که برای همین مشکل مشخص طراحی شده است.

یادآوری مشکل

function StaticFooter() {
  console.log("StaticFooter رندر شد");
  return <footer>تمام حقوق محفوظ است</footer>;
}

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <StaticFooter />
    </div>
  );
}

با هر کلیک، StaticFooter دوباره Render می‌شود، در حالی که هیچ Propی نمی‌گیرد و خروجی‌اش هرگز تغییر نمی‌کند. در بیشتر موارد این بی‌ضرر است، اما اگر StaticFooter محاسبه سنگینی داشته باشد یا خودش زیردرخت بزرگی باشد، این Render تکراری واقعاً هزینه دارد.

پیچیدن کامپوننت با memo

const StaticFooter = React.memo(function StaticFooter() {
  console.log("StaticFooter رندر شد");
  return <footer>تمام حقوق محفوظ است</footer>;
});

با این تغییر، React پیش از Render کردن دوباره StaticFooter، مقدار Props جدید را با Props رندر قبلی مقایسه می‌کند. اگر همه Props برابر باشند (با مقایسه سطحی، یعنی === برای هرکدام)، React از محاسبه دوباره صرف‌نظر می‌کند و همان خروجی قبلی را نگه می‌دارد.

memo فقط والد را بررسی نمی‌کند، فرزند را هم کنترل می‌کند

نکته مهم این است که memo تصمیم را به عهده خود کامپوننت فرزند می‌گذارد، نه والد. یعنی والد مثل قبل هر بار Render می‌شود؛ اما وقتی نوبت به محاسبه خروجی فرزند memo‌شده می‌رسد، React اول چک می‌کند آیا لازم است یا نه.

مقایسه سطحی یعنی چه

memo برای هر Prop، فقط برابری مرجع (Reference Equality) را بررسی می‌کند، نه محتوای عمیق آن. برای مقادیر ساده مثل رشته یا عدد، این کار مشکلی ندارد. اما همان‌طور که در فصل ششم درباره Objectهای تازه‌ساخته‌شده دیدیم، اگر یکی از Propها یک Object، Array یا تابع باشد که در هر Render والد از نو ساخته می‌شود، مقایسه سطحی همیشه «متفاوت» تشخیص می‌دهد و memo هیچ اثری نخواهد داشت:

// memo اینجا فایده‌ای ندارد
<ProductCard style={{ color: "red" }} />

در این کد، style در هر Render والد یک Object کاملاً جدید است؛ حتی با memo، ProductCard باز هم هر بار دوباره Render می‌شود. راه‌حل این مشکل با useMemo و useCallback، موضوع دو درس بعدی است.

memo رایگان نیست

همان‌طور که در درس قبل اشاره شد، memo خودش هزینه دارد: مقایسه Props در هر Render والد. برای کامپوننت‌های ساده و سبک که Render شدنشان تقریباً بی‌هزینه است، اضافه کردن memo ممکن است هیچ سودی نداشته باشد یا حتی کمی کندتر باشد، چون حالا یک مقایسه اضافه هم انجام می‌شود.

چه زمانی memo واقعاً مفید است

memo بیشترین فایده را وقتی دارد که کامپوننت، محاسبه یا زیردرخت نسبتاً سنگینی دارد، Props آن معمولاً بین Renderهای والد ثابت می‌ماند، و والد با فرکانس نسبتاً بالایی Render می‌شود (مثلاً به‌خاطر State دیگری که ربطی به این فرزند ندارد).

مثال قابل اجرا

در این مثال، با هر کلیک روی دکمه شمارنده، کامپوننت memo‌شده دوباره محاسبه نمی‌شود؛ کنسول را باز کنید تا تفاوت را ببینید.

جلوگیری از Re-render فرزند با React.memo
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

React.memo کامپوننتی را می‌پیچد تا فقط زمانی دوباره Render شود که حداقل یکی از Propهایش، با مقایسه سطحی، نسبت به رندر قبلی تغییر کرده باشد. این ابزار برای کامپوننت‌های نسبتاً سنگین با Props پایدار مفید است و برای کامپوننت‌های ساده، معمولاً سودی ندارد؛ همچنین در برابر Object، Array یا تابع تازه‌ساخته‌شده در هر Render، به‌تنهایی کافی نیست. در درس بعدی به Referential Equality می‌پردازیم که پایه فهم همین محدودیت است.