Derived Data و حذف محاسبات غیرضروری

در درس‌های قبل این فصل، ابزارهای memoization را دیدیم که به یک مشکل واقعی و اندازه‌گیری‌شده پاسخ می‌دهند. اما بهترین بهینه‌سازی، گاهی اصلاً نیازی به این ابزارها ندارد؛ چون خودِ نیاز به محاسبه تکراری، از طراحی نادرست State می‌آید. در این درس، به یکی از همان مفاهیم آشنا در فصل سوم، یعنی Derived State، از زاویه Performance برمی‌گردیم.

یادآوری: Derived Data چیست

در فصل سوم دیدیم که برخی مقادیر را نباید به‌عنوان State مستقل نگه داشت، چون می‌توان آن‌ها را مستقیماً از روی State یا Props موجود محاسبه کرد. همان اصل، پایه این درس هم هست؛ فقط این‌بار از زاویه Performance به آن نگاه می‌کنیم.

محاسبه مستقیم، معمولاً کافی است

پیش از رفتن سراغ useMemo، اول باید بپرسیم: آیا اصلاً به این مقدار باید به‌شکل State جداگانه فکر کرد؟ اگر پاسخ منفی است، ساده‌ترین و کم‌هزینه‌ترین راه، محاسبه مستقیم آن در بدنه کامپوننت است، بدون هیچ Hookی:

function Cart({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);
  return <p>مجموع: {total}</p>;
}

برای اکثر آرایه‌های کوچک و متوسط، این محاسبه در هر Render، کاملاً بی‌ضرر و سریع‌تر از هزینه اضافه کردن useMemo است؛ دقیقاً همان نکته‌ای که در درس قبل درباره هزینه مقایسه دیدیم.

چه زمانی محاسبه مستقیم دیگر کافی نیست

وقتی داده به‌قدری بزرگ است که محاسبه، زمان محسوسی می‌برد (مثلاً پردازش روی ده‌ها هزار آیتم)، و این محاسبه واقعاً با هر Render، حتی برای تغییرات بی‌ربط، تکرار می‌شود، تازه آن‌جا useMemo معنا پیدا می‌کند؛ همان چیزی که در درس چهارم این فصل دیدیم.

یک راه‌حل جایگزین: کوچک‌تر کردن وابستگی به Re-render

گاهی به‌جای memoize کردن یک محاسبه سنگین، بهتر است اصلاً از رخ دادن Re-render بی‌ربط جلوگیری کنیم. برای مثال، اگر یک محاسبه فقط به بخشی از State وابسته است، می‌توان آن بخش را در یک کامپوننت جدا و کوچک‌تر قرار داد تا فقط همان بخش، با تغییر مربوطه، دوباره Render شود؛ نه کل صفحه.

// قبل: هر تغییر در App، محاسبه سنگین را هم اجرا می‌کند
function App() {
  const [query, setQuery] = useState("");
  const [theme, setTheme] = useState("light");
  const results = heavySearch(query); // با تغییر theme هم دوباره اجرا می‌شود

  return (
    <div>
      <SearchResults results={results} />
      <ThemeToggle theme={theme} onToggle={setTheme} />
    </div>
  );
}

// بعد: جدا کردن بخش‌های مستقل
function SearchSection() {
  const [query, setQuery] = useState("");
  const results = heavySearch(query);
  return <SearchResults results={results} />;
}

function App() {
  const [theme, setTheme] = useState("light");
  return (
    <div>
      <SearchSection />
      <ThemeToggle theme={theme} onToggle={setTheme} />
    </div>
  );
}

در نسخه دوم، تغییر theme دیگر باعث اجرای heavySearch نمی‌شود، چون آن محاسبه داخل یک کامپوننت کاملاً جدا قرار گرفته که اصلاً به theme وابسته نیست. این راه‌حل، بدون هیچ memoization ای، مشکل اصلی را حل می‌کند.

ترتیب درست تصمیم‌گیری

با کنار هم گذاشتن این فصل، یک ترتیب منطقی به دست می‌آید:

  1. آیا این مقدار اصلاً باید State جداگانه باشد، یا Derived است؟ (فصل سوم)
  2. آیا محاسبه مستقیم، بدون هیچ Hookی، به‌اندازه کافی سریع است؟
  3. آیا می‌توان با جدا کردن ساختار کامپوننت‌ها، از Re-render غیرضروری همان محاسبه جلوگیری کرد؟
  4. فقط اگر هیچ‌کدام کافی نبود و مشکل با اندازه‌گیری تأیید شد، سراغ useMemo برویم.

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

در این مثال، محاسبه جست‌وجو داخل یک کامپوننت جدا قرار گرفته تا با تغییر تم، دوباره اجرا نشود؛ بدون استفاده از useMemo.

جداسازی State به‌جای memoization
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

پیش از استفاده از useMemo، باید بررسی کرد که آیا اصلاً مقدار موردنظر باید State جداگانه باشد، آیا محاسبه مستقیم به‌اندازه کافی سریع است، و آیا جدا کردن ساختار کامپوننت‌ها می‌تواند از Re-render غیرضروری همان محاسبه جلوگیری کند. memoization آخرین ابزار در این فهرست است، نه اولین. در درس بعدی به Profiling با React DevTools می‌پردازیم تا این تصمیم‌ها را با داده واقعی بگیریم.