مقالات › React

بهینه‌سازی عملکرد React؛ رندر مجدد، Memoization، Code Splitting و مجازی‌سازی لیست

اپلیکیشن React را سریع کنید: علت رندرهای مجدد، اندازه‌گیری با Profiler، استفاده‌ی درست از memo و useMemo و useCallback، تقسیم کد، مجازی‌سازی لیست‌های بلند و بهینه‌سازی تصاویر.

بیشتر اپلیکیشن‌های React به‌طور پیش‌فرض سریع‌اند. وقتی نیستند، علت معمولاً یکی از چند الگوی تکراری است: کامپوننت‌هایی که خیلی بیشتر از لازم رندر می‌شوند، کار سنگین در هر رندر، جاوااسکریپت زیاد در بارگذاری اول یا هزاران گره DOM. این راهنما نشان می‌دهد چطور گلوگاه واقعی را پیدا کنید و راه‌حل درست را به کار ببرید؛ بدون اینکه همه‌جا useMemo بپاشید.

اول اندازه بگیرید

بهینه‌سازی کورکورانه وقت را هدر می‌دهد و کد را سخت‌خوان‌تر می‌کند. از این ابزارها استفاده کنید:

  • Profiler در React DevTools — رندرها را ضبط می‌کند و نشان می‌دهد کدام کامپوننت‌ها، چرا و چقدر طول کشیدند. گزینه‌ی «Highlight updates when components render» را فعال کنید تا رندرهای مجدد را زنده ببینید.
  • پنل Performance در Chrome — Taskهای طولانی، Layout Thrashing و زمان اسکریپت.
  • Lighthouse / Core Web Vitals — شاخص‌های واقعی بارگذاری و تعامل (LCP، INP، CLS).

چیزی را درست کنید که اندازه‌گیری‌ها نشان می‌دهند.

چرا کامپوننت‌ها دوباره رندر می‌شوند

یک کامپوننت وقتی دوباره رندر می‌شود که:

  1. State آن تغییر کند،
  2. والدش دوباره رندر شود (به‌طور پیش‌فرض فرزندان هم رندر می‌شوند)،
  3. Contextی که مصرف می‌کند تغییر کند.

رندر مجدد به‌خودی‌خود بد نیست؛ React فقط جاهایی از DOM را تغییر می‌دهد که خروجی واقعاً عوض شده باشد. مشکل وقتی است که رندر پرهزینه باشد یا مدام رخ دهد (تایپ، اسکرول، انیمیشن).

۱. State را نزدیک جایی که استفاده می‌شود نگه دارید

ارزان‌ترین بهینه‌سازی ساختاری است. Stateی که بالای درخت باشد همه‌ی زیرمجموعه را دوباره رندر می‌کند:

// ❌ تایپ در جستجو کل صفحه را دوباره رندر می‌کند
function Page() {
  const [query, setQuery] = useState("");
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <HeavyChart />
      <ArticleGrid />
    </>
  );
}

// ✅ State را به کامپوننت خودش منتقل کنید
function SearchBox() {
  const [query, setQuery] = useState("");
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

یک ترفند دیگر: زیردرخت‌های سنگین را به‌صورت children پاس بدهید؛ آن‌ها توسط والد ساخته می‌شوند و با تغییر State کامپوننت دربرگیرنده دوباره رندر نمی‌شوند:

function ScrollTracker({ children }) {
  const [y, setY] = useState(0);
  // ... y را با اسکرول به‌روز می‌کند
  return <div data-y={y}>{children}</div>;   // children با اسکرول رندر نمی‌شوند
}

۲. React.memo برای کامپوننت‌های خالص

React.memo وقتی Propها به‌صورت سطحی برابر باشند، رندر مجدد کامپوننت را رد می‌کند:

const ArticleCard = memo(function ArticleCard({ article, onOpen }) {
  return (
    <article onClick={() => onOpen(article.slug)}>
      <h3>{article.title}</h3>
    </article>
  );
});

فقط وقتی مفید است که Propها پایدار باشند. یک شیء یا تابع جدید در هر رندر والد اثرش را خنثی می‌کند؛ اینجاست که useMemo و useCallback وارد می‌شوند.

۳. useMemo و useCallback — با هدف

function ArticleList({ articles, tag, onOpen }) {
  const visible = useMemo(
    () => articles.filter((a) => !tag || a.tag === tag).sort(byDateDesc),
    [articles, tag]
  );

  const handleOpen = useCallback((slug) => onOpen(slug), [onOpen]);

  return visible.map((a) => <ArticleCard key={a.id} article={a} onOpen={handleOpen} />);
}

وقتی از آن‌ها استفاده کنید که:

  • محاسبه واقعاً پرهزینه است (قابل اندازه‌گیری)، یا
  • یک مقدار/تابع به فرزند Memo‌شده داده می‌شود یا وابستگی یک Effect است.

برای محاسبات ارزان «محض احتیاط» استفاده نکنید؛ Memoization هزینه‌ی خودش را دارد و کد را شلوغ می‌کند. React Compiler حالا می‌تواند بیشتر این Memoizationها را خودکار اضافه کند؛ اگر پروژه‌تان از آن استفاده می‌کند، کد ساده بنویسید و بهینه‌سازی را به کامپایلر بسپارید.

۴. key پایدار در لیست‌ها

// ❌ key با اندیس، هنگام درج یا جابه‌جایی State را خراب و کار اضافه ایجاد می‌کند
items.map((item, i) => <Row key={i} item={item} />)

// ✅ شناسه‌ی یکتای پایدار
items.map((item) => <Row key={item.id} item={item} />)

۵. Contextها را تقسیم کنید و از Providerهای غول‌پیکر پرهیز کنید

وقتی مقدار یک Context تغییر می‌کند، همه‌ی مصرف‌کننده‌ها دوباره رندر می‌شوند. Contextها را بر اساس فراوانی تغییر جدا کنید (ThemeContext در برابر CartContext)، مقدار Provider را Memo کنید یا برای State پرتغییر از یک Store با Selector (Zustand، Redux) استفاده کنید.

۶. Code Splitting: جاوااسکریپت کمتری بفرستید

کاربر نباید برای خواندن یک مقاله پنل مدیریت را دانلود کند:

const AdminPanel = lazy(() => import("./admin/AdminPanel"));
const ArticlePage = lazy(() => import("./pages/ArticlePage"));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/admin/*" element={<AdminPanel />} />
    <Route path="/articles/:slug" element={<ArticlePage />} />
  </Routes>
</Suspense>

همچنین:

  • فقط چیزی را Import کنید که استفاده می‌کنید (import { debounce } from "lodash-es"، نه کل کتابخانه).
  • Bundleها را با rollup-plugin-visualizer یا vite-bundle-visualizer تحلیل کنید.
  • کتابخانه‌های سنگین (نمودار، ویرایشگر، هایلایت کد) را داخل همان کامپوننتی که لازمشان دارد Lazy بارگذاری کنید.

در همین سایت، انتقال Parse کردن Markdown و هایلایت کد به صفحه‌ی مقاله‌ی Lazy‌شده، Bundle اصلی را حدود ۴۰ درصد کوچک کرد.

۷. لیست‌های بلند را مجازی کنید

رندر ۵۰۰۰ ردیف یعنی ۵۰۰۰ گره DOM. مجازی‌سازی (Virtualization) فقط ردیف‌های قابل دیدن را رندر می‌کند:

import { useVirtualizer } from "@tanstack/react-virtual";

function LogList({ rows }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
  });

  return (
    <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((v) => (
          <div key={v.key} style={{ position: "absolute", top: 0, transform: `translateY(${v.start}px)`, height: v.size, width: "100%" }}>
            {rows[v.index].message}
          </div>
        ))}
      </div>
    </div>
  );
}

برای لیست‌های نه‌چندان بلند، صفحه‌بندی یا «بارگذاری بیشتر» اغلب ساده‌تر و تجربه‌ی کاربری بهتری است.

۸. تعامل را با Transition روان نگه دارید

const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState(allArticles);

function handleChange(e) {
  setQuery(e.target.value);                  // فوری: به‌روزرسانی ورودی
  startTransition(() => {
    setResults(filterArticles(allArticles, e.target.value)); // غیرفوری
  });
}

useTransition (یا useDeferredValue) به React اجازه می‌دهد تایپ را روان نگه دارد درحالی‌که یک لیست سنگین در پس‌زمینه رندر می‌شود؛ مستقیماً شاخص INP را بهبود می‌دهد.

۹. رویدادهای پرهزینه را Debounce و Throttle کنید

جستجو حین تایپ و Handlerهای Resize و Scroll می‌توانند ده‌ها بار در ثانیه اجرا شوند. درخواست‌های شبکه را Debounce و Handlerهای اسکرول را Throttle کنید یا برای تشخیص قابل دیدن بودن به‌جای رویداد اسکرول از IntersectionObserver استفاده کنید.

۱۰. تصاویر و فایل‌ها

اغلب بزرگ‌ترین بهبود اصلاً ربطی به React ندارد:

  • قالب‌های مدرن (WebP/AVIF) را در اندازه‌ی درست ارائه دهید و از srcset استفاده کنید.
  • برای تصاویر پایین صفحه loading="lazy" و برای جلوگیری از جابه‌جایی چیدمان width/height صریح بگذارید.
  • تصویر اصلی بالای صفحه را پیش‌بارگذاری کنید (fetchpriority="high").
  • فونت‌ها را روی سرور خودتان میزبانی و زیرمجموعه‌سازی کنید و از font-display: swap استفاده کنید.

۱۱. ویژگی‌های ارزان را انیمیت کنید

transform و opacity را انیمیت کنید؛ این‌ها توسط Compositor انجام می‌شوند. انیمیت کردن width، top یا box-shadow در هر فریم Layout یا Paint ایجاد می‌کند. کتابخانه‌هایی مثل GSAP این کار را ساده می‌کنند و prefers-reduced-motion باید حرکت‌های غیرضروری را غیرفعال کند.

چک‌لیست

علامت راه‌حل احتمالی
تایپ کند به نظر می‌رسد انتقال State به پایین، useTransition، Debounce
کل صفحه با تغییرات کوچک رندر می‌شود تقسیم کامپوننت/Context، memo
بارگذاری اول کند است Code Splitting، Bundle کوچک‌تر، بهینه‌سازی تصاویر
لیست بزرگ بد اسکرول می‌شود مجازی‌سازی یا صفحه‌بندی
انیمیشن‌ها پرپرش‌اند انیمیت transform/opacity، پرهیز از Layout Thrashing

جمع‌بندی

قبل از بهینه‌سازی اندازه بگیرید. بعد اول ساختار را درست کنید — State محلی، تقسیم Contextها، پاس دادن children — و فقط جایی سراغ memo، useMemo و useCallback بروید که Profiler هزینه‌ی واقعی نشان می‌دهد. با Code Splitting جاوااسکریپت کمتری بفرستید، لیست‌های بزرگ را مجازی کنید، تعامل‌ها را با Transition روان نگه دارید و تصاویر، فونت‌ها و ویژگی‌های انیمیشن را فراموش نکنید. اپلیکیشن‌های React سریع بیشتر نتیجه‌ی معماری خوب‌اند، نه بهینه‌سازی‌های ریز.

پرسش‌های متداول

آیا هر کامپوننت را در React.memo بپیچم؟

نه. هزینه‌ی مقایسه و پیچیدگی اضافه می‌کند. آن را برای کامپوننت‌هایی استفاده کنید که با Propهای یکسان زیاد رندر می‌شوند و رندرشان پرهزینه است.

آیا StrictMode برنامه را کند می‌کند؟

فقط در محیط توسعه، جایی که عمداً برخی توابع را دو بار اجرا می‌کند. Build نهایی تأثیری نمی‌پذیرد.

آیا React از فریم‌ورک‌های دیگر کندتر است؟

در اپلیکیشن‌های معمولی تفاوت به‌ندرت محسوس است. معماری، حجم Bundle و بهینه‌سازی فایل‌ها خیلی بیشتر از انتخاب فریم‌ورک اهمیت دارند.