بهینهسازی عملکرد React: تکنیکهای پیشرفته برای اپلیکیشنهایی با سرعت برقآسا
عملکرد (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