SEPEHR.SYS

Starting SEPEHR.SYS…

Loading

سپهر محسنی

مهندس نرم‌افزار فول‌استک و هوش مصنوعی

NOTES.TXT ×

بهینه‌سازی عملکرد React: تکنیک‌های پیشرفته برای اپلیکیشن‌هایی با سرعت برق‌آسا

8′ · ۲۷ دی ۱۴۰۴

عملکرد (Performance) نقشی حیاتی در تجربهٔ کاربری و سئو دارد. یک اپلیکیشن React کُند می‌تواند به نرخ پرش (Bounce Rate) بالاتر و نرخ تبدیل پایین‌تر منجر شود. در این راهنما تکنیک‌های پیشرفته‌ای را بررسی می‌کنیم که به کمک آن‌ها می‌توانید اپلیکیشن‌های React خود را برای رسیدن به بیشترین عملکرد بهینه کنید.

درک رفتار رندر در React

پیش از آنکه سراغ بهینه‌سازی بروید، باید بدانید React چگونه دربارهٔ رندر مجدد کامپوننت‌ها تصمیم می‌گیرد:

// Every state change triggers a re-render of this component
// AND all its children
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild /> {/* Re-renders even though it doesn't use count! */}
    </div>
  );
}

<Callout type="warning"> وقتی یک کامپوننت والد دوباره رندر می‌شود، React همهٔ کامپوننت‌های فرزند آن را نیز دوباره رندر می‌کند؛ فارغ از اینکه props آن‌ها تغییر کرده باشد یا نه. همین موضوع، اصلی‌ترین منشأ مشکلات عملکردی است. </Callout>

تکنیک‌های Memoization

React.memo برای مموایز کردن کامپوننت‌ها

با React.memo از رندرهای مجدد غیرضروری جلوگیری کنید:

import { memo } from 'react';

// Without memo - re-renders on every parent render
function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard rendered');
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
}

// With memo - only re-renders when props change
const MemoizedProductCard = memo(ProductCard);

// Custom comparison function for complex props
const MemoizedProductCardCustom = memo(ProductCard, (prevProps, nextProps) => {
  return prevProps.product.id === nextProps.product.id &&
         prevProps.product.price === nextProps.product.price;
});

useMemo برای محاسبات سنگین

import { useMemo } from 'react';

function ProductList({ products, filters }) {
  // Without useMemo - recalculates on every render
  // const filteredProducts = products.filter(p => 
  //   p.category === filters.category && p.price <= filters.maxPrice
  // );

  // With useMemo - only recalculates when dependencies change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products.filter(p => 
      p.category === filters.category && 
      p.price <= filters.maxPrice
    );
  }, [products, filters.category, filters.maxPrice]);

  // Expensive sorting operation
  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort((a, b) => {
      if (filters.sortBy === 'price') return a.price - b.price;
      if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
      return 0;
    });
  }, [filteredProducts, filters.sortBy]);

  return (
    <div className="product-grid">
      {sortedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

useCallback برای مرجع پایدار توابع

import { useCallback, useState } from 'react';

function ShoppingCart() {
  const [items, setItems] = useState([]);

  // Without useCallback - new function on every render
  // const handleRemove = (id) => {
  //   setItems(items => items.filter(item => item.id !== id));
  // };

  // With useCallback - stable reference
  const handleRemove = useCallback((id) => {
    setItems(items => items.filter(item => item.id !== id));
  }, []); // Empty deps - function never changes

  const handleUpdateQuantity = useCallback((id, quantity) => {
    setItems(items => items.map(item => 
      item.id === id ? { ...item, quantity } : item
    ));
  }, []);

  return (
    <div>
      {items.map(item => (
        <CartItem
          key={item.id}
          item={item}
          onRemove={handleRemove}
          onUpdateQuantity={handleUpdateQuantity}
        />
      ))}
    </div>
  );
}

// CartItem can now be memoized effectively
const CartItem = memo(function CartItem({ item, onRemove, onUpdateQuantity }) {
  return (
    <div className="cart-item">
      <span>{item.name}</span>
      <input
        type="number"
        value={item.quantity}
        onChange={(e) => onUpdateQuantity(item.id, parseInt(e.target.value))}
      />
      <button onClick={() => onRemove(item.id)}>Remove</button>
    </div>
  );
});

<Note title="نکتهٔ عملکردی"> از useMemo و useCallback فقط زمانی استفاده کنید که وجود یک مشکل عملکردی را با اندازه‌گیری تأیید کرده باشید. بهینه‌سازی زودهنگام می‌تواند بدون هیچ فایدهٔ چشمگیری، خواندن کد را دشوارتر کند. </Note>

Code Splitting و Lazy Loading

Code Splitting مبتنی بر Route

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Admin = lazy(() => import('./pages/Admin'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/checkout" element={<Checkout />} />
          <Route path="/admin/*" element={<Admin />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

function PageLoader() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500" />
    </div>
  );
}

Code Splitting در سطح کامپوننت

import { lazy, Suspense, useState } from 'react';

// Heavy components loaded on demand
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
const ImageGallery = lazy(() => import('./components/ImageGallery'));

function ProductEditor({ product }) {
  const [showCharts, setShowCharts] = useState(false);

  return (
    <div>
      <h1>Edit Product</h1>
      
      {/* Editor loads when component mounts */}
      <Suspense fallback={<div>Loading editor...</div>}>
        <RichTextEditor 
          content={product.description}
          onChange={handleDescriptionChange}
        />
      </Suspense>

      {/* Charts only load when user clicks */}
      <button onClick={() => setShowCharts(true)}>
        Show Analytics
      </button>
      
      {showCharts && (
        <Suspense fallback={<div>Loading charts...</div>}>
          <ChartDashboard productId={product.id} />
        </Suspense>
      )}
    </div>
  );
}

مجازی‌سازی (Virtualization) برای لیست‌های بزرگ

استفاده از react-window برای رندر بهینه

import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function VirtualizedProductList({ products }) {
  const Row = ({ index, style }) => {
    const product = products[index];
    return (
      <div style={style} className="flex items-center p-4 border-b">
        <img 
          src={product.thumbnail} 
          alt={product.name}
          className="w-16 h-16 object-cover rounded"
        />
        <div className="ml-4">
          <h3 className="font-semibold">{product.name}</h3>
          <p className="text-gray-600">${product.price}</p>
        </div>
      </div>
    );
  };

  return (
    <div className="h-[600px] w-full">
      <AutoSizer>
        {({ height, width }) => (
          <List
            height={height}
            width={width}
            itemCount={products.length}
            itemSize={80}
          >
            {Row}
          </List>
        )}
      </AutoSizer>
    </div>
  );
}

لیست با اندازهٔ متغیر برای محتوای پویا

import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback } from 'react';

function ChatMessages({ messages }) {
  const listRef = useRef();
  const rowHeights = useRef({});

  const getRowHeight = useCallback((index) => {
    return rowHeights.current[index] || 60;
  }, []);

  const setRowHeight = useCallback((index, size) => {
    rowHeights.current[index] = size;
    listRef.current?.resetAfterIndex(index);
  }, []);

  const Row = ({ index, style }) => {
    const rowRef = useRef();
    const message = messages[index];

    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.getBoundingClientRect().height);
      }
    }, [index, setRowHeight]);

    return (
      <div style={style}>
        <div ref={rowRef} className="p-4">
          <p className="font-semibold">{message.sender}</p>
          <p>{message.content}</p>
          <span className="text-xs text-gray-500">{message.timestamp}</span>
        </div>
      </div>
    );
  };

  return (
    <List
      ref={listRef}
      height={500}
      width="100%"
      itemCount={messages.length}
      itemSize={getRowHeight}
      estimatedItemSize={60}
    >
      {Row}
    </List>
  );
}

بهینه‌سازی مدیریت State

جلوگیری از رندرهای مجدد غیرضروری در Context

import { createContext, useContext, useState, useMemo } from 'react';

// Split context by update frequency
const UserContext = createContext();
const UserActionsContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  // Memoize actions to prevent re-renders
  const actions = useMemo(() => ({
    login: async (credentials) => {
      const user = await authService.login(credentials);
      setUser(user);
    },
    logout: () => {
      authService.logout();
      setUser(null);
    },
    updateProfile: async (data) => {
      const updated = await userService.update(data);
      setUser(updated);
    },
  }), []);

  return (
    <UserContext.Provider value={user}>
      <UserActionsContext.Provider value={actions}>
        {children}
      </UserActionsContext.Provider>
    </UserContext.Provider>
  );
}

// Components only subscribe to what they need
function UserAvatar() {
  const user = useContext(UserContext); // Only re-renders when user changes
  return <img src={user?.avatar} alt={user?.name} />;
}

function LogoutButton() {
  const { logout } = useContext(UserActionsContext); // Never re-renders!
  return <button onClick={logout}>Logout</button>;
}

استفاده از Zustand برای مدیریت کارآمد state

import { create } from 'zustand';
import { shallow } from 'zustand/shallow';

const useStore = create((set, get) => ({
  products: [],
  cart: [],
  filters: { category: 'all', maxPrice: 1000 },
  
  setProducts: (products) => set({ products }),
  
  addToCart: (product) => set((state) => ({
    cart: [...state.cart, { ...product, quantity: 1 }]
  })),
  
  updateFilters: (filters) => set((state) => ({
    filters: { ...state.filters, ...filters }
  })),
  
  // Computed values
  get cartTotal() {
    return get().cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  },
}));

// Select only needed state to prevent unnecessary re-renders
function CartIcon() {
  const itemCount = useStore((state) => state.cart.length);
  return <span className="badge">{itemCount}</span>;
}

function ProductFilters() {
  // Use shallow comparison for object selections
  const { filters, updateFilters } = useStore(
    (state) => ({ filters: state.filters, updateFilters: state.updateFilters }),
    shallow
  );
  
  return (
    <div>
      <select 
        value={filters.category}
        onChange={(e) => updateFilters({ category: e.target.value })}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
    </div>
  );
}

بهینه‌سازی تصاویر

بارگذاری تنبل (Lazy Loading) تصاویر

function OptimizedImage({ src, alt, className }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={imgRef} className={`relative ${className}`}>
      {/* Placeholder */}
      {!isLoaded && (
        <div className="absolute inset-0 bg-gray-200 animate-pulse" />
      )}
      
      {/* Actual image */}
      {isInView && (
        <img
          src={src}
          alt={alt}
          className={`transition-opacity duration-300 ${
            isLoaded ? 'opacity-100' : 'opacity-0'
          }`}
          onLoad={() => setIsLoaded(true)}
          loading="lazy"
        />
      )}
    </div>
  );
}

پروفایل‌گیری و اندازه‌گیری عملکرد

استفاده از React DevTools Profiler

import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  // Log slow renders
  if (actualDuration > 16) { // More than one frame (60fps)
    console.warn(`Slow render in ${id}:`, {
      phase,
      actualDuration: `${actualDuration.toFixed(2)}ms`,
      baseDuration: `${baseDuration.toFixed(2)}ms`,
    });
  }
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Header />
      <Profiler id="ProductList" onRender={onRenderCallback}>
        <ProductList />
      </Profiler>
      <Footer />
    </Profiler>
  );
}

هوک‌های سفارشی برای سنجش عملکرد

function useRenderCount(componentName) {
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered ${renderCount.current} times`);
  });
}

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log(`[${name}] Changed props:`, changedProps);
      }
    }

    previousProps.current = props;
  });
}

جمع‌بندی

بهینه‌سازی عملکرد در React یعنی شناخت چرخهٔ رندر و به‌کارگیری تکنیک درست در زمان درست. کار را با اندازه‌گیری به کمک React DevTools Profiler آغاز کنید، گلوگاه‌ها را شناسایی کنید و سپس بهینه‌سازی‌های هدفمند انجام دهید.

نکات کلیدی:

  • از React.memo، useMemo و useCallback آگاهانه و هدفمند استفاده کنید
  • برای اپلیکیشن‌های بزرگ Code Splitting پیاده‌سازی کنید
  • لیست‌های طولانی را مجازی‌سازی کنید
  • مدیریت state را طوری بهینه کنید که رندرهای مجدد به حداقل برسد
  • همیشه پیش و پس از بهینه‌سازی اندازه‌گیری کنید

این یادداشت ترجمهٔ فارسی نوشتهٔ خودم است — نسخهٔ اصلی (انگلیسی) در dev.to