Performance Optimization
React.memo
Ngăn component re-render khi props không thay đổi (shallow comparison).
import { memo, useState } from 'react';
interface ProductCardProps {
id: number;
name: string;
price: number;
onAddToCart: (id: number) => void;
}
// ✅ Wrap component với memo
const ProductCard = memo(function ProductCard({
id,
name,
price,
onAddToCart,
}: ProductCardProps) {
console.log(`Rendering ProductCard ${id}`);
return (
<div className="product-card">
<h3>{name}</h3>
<p>${price}</p>
<button onClick={() => onAddToCart(id)}>Add to Cart</button>
</div>
);
});
// Khi nào memo có tác dụng?
function App() {
const [cartCount, setCartCount] = useState(0);
const [products] = useState([...]);
// ❌ Handler mới mỗi render → memo vô tác dụng!
// const handleAddToCart = (id) => setCartCount(c => c + 1);
// ✅ Stable reference với useCallback
const handleAddToCart = useCallback((id: number) => {
setCartCount((c) => c + 1);
addToCart(id);
}, []);
return (
<div>
<p>Cart: {cartCount}</p>
{products.map((p) => (
<ProductCard
key={p.id}
{...p}
onAddToCart={handleAddToCart}
/>
))}
</div>
);
}
// Custom comparison function
const MemoizedList = memo(
function HeavyList({ items, searchQuery }) { ... },
(prevProps, nextProps) => {
// true = không re-render, false = re-render
return (
prevProps.searchQuery === nextProps.searchQuery &&
prevProps.items.length === nextProps.items.length
);
}
);
Code Splitting & Lazy Loading
Chia nhỏ bundle, chỉ load code khi cần.
import { lazy, Suspense, startTransition } from 'react';
// Lazy load pages (route-based splitting)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
// Lazy load heavy components
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const DataGrid = lazy(() =>
import('./components/DataGrid').then((module) => ({
default: module.DataGrid, // Named export
}))
);
function App() {
const [showEditor, setShowEditor] = useState(false);
return (
<Suspense fallback={<div className="page-spinner">Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
<button
onClick={() => startTransition(() => setShowEditor(true))}
>
Open Editor
</button>
{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<RichTextEditor />
</Suspense>
)}
</Suspense>
);
}
Virtualization - Render Danh sách Lớn
Chỉ render các items hiển thị trong viewport.
npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60, // Estimated row height
overscan: 5, // Render thêm items ngoài viewport
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }}
>
{/* Total height container */}
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ItemRow item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
Tránh Re-renders Không Cần Thiết
// 1. State colocation - đặt state gần nơi dùng
// ❌ State ở root khiến toàn bộ app re-render
function App() {
const [inputValue, setInputValue] = useState(''); // Không cần ở đây
return (
<>
<SearchBar value={inputValue} onChange={setInputValue} />
<HeavyComponent /> {/* Re-render mỗi khi type */}
</>
);
}
// ✅ State trong component cần nó
function App() {
return (
<>
<SearchBar /> {/* Tự quản lý state */}
<HeavyComponent /> {/* Không re-render */}
</>
);
}
// 2. Children trick - tránh re-render khi parent thay đổi
function SlowParent({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
{children} {/* children không re-render khi count thay đổi */}
</div>
);
}
function App() {
return (
<SlowParent>
<HeavyComponent /> {/* Không bị ảnh hưởng bởi SlowParent state */}
</SlowParent>
);
}
// 3. Tách components nhỏ
// ❌ Cả form re-render khi count thay đổi
function ProductPage() {
const [count, setCount] = useState(0);
const [formData, setFormData] = useState({...});
return (
<div>
<Counter count={count} onChange={setCount} />
<HeavyForm formData={formData} onChange={setFormData} />
</div>
);
}
React Profiler
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (
id, // Component tree identifier
phase, // "mount" hoặc "update"
actualDuration, // Thời gian render (ms)
baseDuration, // Ước tính không có memo
startTime,
commitTime
) => {
if (actualDuration > 16) { // Slower than 60fps
console.warn(`Slow render in ${id}: ${actualDuration.toFixed(2)}ms`);
}
};
function App() {
return (
<Profiler id="ProductList" onRender={onRender}>
<ProductList />
</Profiler>
);
}
Web Workers cho Tính toán Nặng
// worker.ts
self.onmessage = (e: MessageEvent) => {
const { data } = e;
const result = heavyCalculation(data);
self.postMessage(result);
};
// Component
function DataAnalysis({ rawData }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(
new URL('./worker.ts', import.meta.url),
{ type: 'module' }
);
worker.postMessage(rawData);
worker.onmessage = (e) => {
setResult(e.data);
worker.terminate();
};
return () => worker.terminate();
}, [rawData]);
return result ? <Chart data={result} /> : <Spinner />;
}
Checklist Performance
□ Dùng React DevTools Profiler để identify bottlenecks
□ Áp dụng React.memo cho components render nhiều
□ useCallback cho callbacks truyền vào memo components
□ useMemo cho tính toán nặng
□ Route-based code splitting với lazy()
□ Virtualize danh sách dài (>100 items)
□ Tránh tạo object/array mới trong JSX (gây re-render)
□ Đặt state gần nơi sử dụng (state colocation)
□ Dùng Suspense boundaries hợp lý
□ Optimize images (lazy loading, correct size)
□ Enable gzip/brotli compression ở server