EN/CH Mode
BRUCE_FE Interview Notes - React Render Optimization (memo, useMemo, useCallback)
In-depth analysis of React's memo, useMemo and useCallback performance optimization techniques, including practical examples, performance measurement methods, trade-off analysis and complete answers to common interview questions.
EN/CH Mode

Lazy to read articles? Then watch videos!
Basic Concepts
React component rendering mechanism is a common performance bottleneck in frontend development. When a parent component re-renders, all child components will re-render by default, regardless of whether their props have changed. React provides three main tools to avoid unnecessary rendering:
- 1.
React.memo: Memoize entire component - 2.
useMemo: Memoize computation results - 3.
useCallback: Memoize functions
React.memo: Component Level Optimization
React.memo is a higher-order component that only re-renders when the passed props change.
// Unoptimized version
function ProductItem({ product, onAddToCart }) {
console.log(`Rendering ${product.name}`);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
</div>
);
}
// Optimized version
const MemoizedProductItem = React.memo(ProductItem);Note: When using objects or functions as props, each parent component render creates new references, making memo ineffective.
useMemo: Memoize computation results
useMemo is used to avoid repeating expensive computations on every render.
function ProductList({ products, filter }) {
// Unoptimized: recalculate on every render
// const filteredProducts = products.filter(p => p.name.includes(filter));
// Optimized: only recalculate when products or filter changes
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p => p.name.includes(filter));
}, [products, filter]);
return (
<div>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
}useCallback: Memoize functions
useCallback is used to memoize functions, avoiding creating new function references on every render.
function ShoppingCart() {
const [items, setItems] = useState([]);
// Unoptimized: new function on every render
// const handleAddItem = (id) => {
// setItems(prev => [...prev, id]);
// };
// Optimized: new function only when setItems changes
const handleAddItem = useCallback((id) => {
setItems(prev => [...prev, id]);
}, [setItems]);
return (
<div>
<h2>Cart ({items.length})</h2>
<ProductList onAddItem={handleAddItem} />
</div>
);
}Performance Optimization Decision Tree
Common Pitfalls and Considerations
Understanding memoization pitfalls can help you avoid common performance optimization mistakes:
- 1. Over-optimization: Don't add memoization to every component or function, as it adds its own performance overhead.
- 2. Missing dependencies: Omitting dependencies in useMemo and useCallback can lead to bugs.
- 3. Object/Array reference issues: Creating objects/arrays directly in render makes memo ineffective.
// Wrong example
<MemoizedComponent data={{id: 1}} /> // New object on every render
// Correct example
const data = useMemo(() => ({id: 1}), []);
<MemoizedComponent data={data} />Trade-offs between Memoization and Re-rendering
Understanding when to use memoization techniques is key to React performance optimization. Premature optimization can be ineffective or even counterproductive.
Measuring Optimization Benefits
Before deciding to use memoization, first measure performance:
import { useEffect, useState } from 'react';
function RenderMetrics({ componentName }) {
const [renderCount, setRenderCount] = useState(0);
const [renderTime, setRenderTime] = useState(0);
useEffect(() => {
const start = performance.now();
setRenderCount(prev => prev + 1);
return () => {
const time = performance.now() - start;
setRenderTime(time);
};
});
return (
<div className="text-xs text-gray-400">
{componentName}: {renderCount} renders, latest took {renderTime.toFixed(2)}ms
</div>
);
}Cost-Benefit Analysis of Memoization
// Scenario 1: Simple component, low optimization benefit
function SimpleLabel({ text }) {
return <p>{text}</p>;
}
// No memo needed - rendering cost is minimal, memoization adds overhead
// Scenario 2: Medium complexity, case-by-case
function UserCard({ user, onEdit }) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
}
// If parent updates frequently but user rarely changes, memo might be beneficial
// Scenario 3: Complex component, high optimization benefit
function DataTable({ rows, columns, onRowSelect }) {
// Large data and complex DOM
return (
<table>
{/* Complex rendering logic */}
</table>
);
}
// Strongly recommend using memo, rendering cost is expensive⚠️ Practical Guidelines to Avoid Over-optimization
- 1. Measure First, Optimize Later: Use React DevTools Profiler or
console.time()to measure actual performance - 2. 80/20 Rule: Optimize the 20% of components that have the biggest impact on user experience
- 3. Consider Memoization Cost: Increased memory usage, dependency comparison time, and code complexity
Remember: Premature optimization is the root of all evil. Optimization should solve confirmed problems, not be a preventive measure.
Analysis of High-frequency Re-rendering Scenarios
Here are two common high-frequency re-rendering scenarios in real development, and how to measure and solve these performance issues:
Scenario 1: Large Data Chart Rendering
When multiple charts based on the same large dataset need to be displayed on the same page, each data update may cause all charts to recalculate and re-render.
Optimization Solution:
// Optimized large data chart rendering
function DashboardPage() {
const [salesData, setSalesData] = useState([]);
const [filterDate, setFilterDate] = useState('last30days');
// Fetch sales data
useEffect(() => {
fetchSalesData(filterDate).then(data => setSalesData(data));
}, [filterDate]);
// Use useMemo to memoize data processing results
const processedData = useMemo(() => {
console.time('Data processing time');
const result = salesData.map(item => ({
...item,
revenue: calculateRevenue(item),
growth: calculateGrowth(item)
}));
console.timeEnd('Data processing time');
return result;
}, [salesData]); // Only recalculate when salesData changes
// Memoize data aggregation for each chart
const pieChartData = useMemo(() =>
aggregateForPieChart(processedData),
[processedData]);
const barChartData = useMemo(() =>
aggregateForBarChart(processedData),
[processedData]);
const lineChartData = useMemo(() =>
aggregateForLineChart(processedData),
[processedData]);
// Use React.memo to optimize chart components
const MemoizedPieChart = React.memo(PieChart);
const MemoizedBarChart = React.memo(BarChart);
const MemoizedLineChart = React.memo(LineChart);
return (
<div className="dashboard">
<DateFilter value={filterDate} onChange={setFilterDate} />
<div className="charts-grid">
<MemoizedPieChart data={pieChartData} />
<MemoizedBarChart data={barChartData} />
<MemoizedLineChart data={lineChartData} />
<MemoizedDataTable data={processedData.slice(0, 100)} />
</div>
</div>
);
}Effect Comparison: Before optimization, each filter condition change or parent component render would recalculate all data and redraw all charts (350ms+). After optimization, recalculation only happens when data actually changes (first time might still take 350ms, but subsequent filtering of the same date range is almost 0ms).
Scenario 2: WebSocket Exchange Order Book
Exchange order book receives high-frequency updates via WebSocket (potentially dozens per second), each update triggers component re-rendering.
Optimization Solution:
// Optimized order book
function OrderBook() {
const [orderBook, setOrderBook] = useState({ bids: [], asks: [] });
const [selectedPair, setSelectedPair] = useState('BTC-USDT');
// Use useRef to store latest data, avoid unnecessary renders
const orderBookRef = useRef(orderBook);
// Use throttle to control update frequency
const updateOrderBook = useCallback(throttle((newData) => {
setOrderBook(newData);
}, 100), []); // Update UI at most once every 100ms
// Connect WebSocket
useEffect(() => {
const ws = new WebSocket('wss://exchange.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
method: 'subscribe',
params: [`orderbook.${selectedPair}`]
}));
};
// High-frequency data updates
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.channel === `orderbook.${selectedPair}`) {
// Update ref to reflect latest data immediately (no render)
orderBookRef.current = data.data;
// Throttle state updates to control render frequency
updateOrderBook(data.data);
}
};
return () => {
ws.close();
updateOrderBook.cancel(); // Clean up throttle function
};
}, [selectedPair, updateOrderBook]);
// Memoize computed values
const bookStats = useMemo(() => {
console.time('Order book calculation');
const totalBidVolume = orderBook.bids.reduce((sum, [_, amount]) => sum + amount, 0);
const totalAskVolume = orderBook.asks.reduce((sum, [_, amount]) => sum + amount, 0);
const spreadValue = orderBook.asks[0]?.[0] - orderBook.bids[0]?.[0] || 0;
const spreadPercentage = (spreadValue / orderBook.bids[0]?.[0]) * 100 || 0;
console.timeEnd('Order book calculation');
return {
totalBidVolume,
totalAskVolume,
spreadValue,
spreadPercentage
};
}, [orderBook]);
// Memoize child components
const MemoizedOrderTable = React.memo(OrderTable);
const MemoizedDepthChart = React.memo(DepthChart);
return (
<div className="order-book">
<h2>{selectedPair} Order Book</h2>
<div className="stats">
<div>Spread: {bookStats.spreadValue.toFixed(2)} ({bookStats.spreadPercentage.toFixed(2)}%)</div>
<div>Bid Volume: {bookStats.totalBidVolume.toFixed(2)}</div>
<div>Ask Volume: {bookStats.totalAskVolume.toFixed(2)}</div>
</div>
<div className="book-tables">
<MemoizedOrderTable type="asks" orders={orderBook.asks.slice(0, 15)} />
<MemoizedOrderTable type="bids" orders={orderBook.bids.slice(0, 15)} />
</div>
<MemoizedDepthChart bids={orderBook.bids} asks={orderBook.asks} />
</div>
);
}Effect Comparison: Before optimization, each WebSocket message (potentially dozens per second) would trigger a complete render, causing page stuttering and high CPU usage. After optimization, render frequency is controlled through throttling (maximum 10 times per second), and useMemo and React.memo are used to avoid repeated calculations and unnecessary child component renders, greatly improving interaction smoothness.
These two scenarios are common performance bottlenecks in frontend development. By identifying problems with appropriate measurement tools (such as console.time, React Profiler) and combining techniques like useMemo, React.memo, useCallback and throttling, performance issues caused by high-frequency rendering can be effectively resolved.
🔥 Common Interview Questions
(1) What are the differences between React.memo, useMemo and useCallback?
Answer:
- 1. React.memo: Higher-order component, memoizes entire React component, skips rendering when props haven't changed.
- 2. useMemo: Hook, memoizes computation results (can be any value), doesn't recalculate when dependencies haven't changed.
- 3. useCallback: Hook, memoizes function references, doesn't create new functions when dependencies haven't changed.
Key differences: React.memo optimizes component rendering; useMemo optimizes value computation; useCallback optimizes function references.
(2) When to use React.memo and what to watch out for?
Answer:
When to use:
- 1. Pure display components that re-render frequently
- 2. Components receiving props that remain unchanged for long periods
- 3. Components with expensive rendering costs
Considerations:
// Problem: Passing anonymous function, new reference each time
function Parent() {
return <MemoChild onClick={() => console.log('click')} />;
}
// Solution: Use useCallback to memoize function
function Parent() {
const handleClick = useCallback(() => console.log('click'), []);
return <MemoChild onClick={handleClick} />;
}(3) Why doesn't memo work in the following code? How to fix it?
Answer:
function App() {
const [count, setCount] = useState(0);
// Problem: options is a new object on every render
const options = { theme: 'dark' };
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedComponent options={options} />
</>
);
}
// Fix:
function App() {
const [count, setCount] = useState(0);
// Solution: Use useMemo to memoize object
const options = useMemo(() => ({ theme: 'dark' }), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedComponent options={options} />
</>
);
}(4) How to determine whether to use memo, useMemo or useCallback?
Answer:
Decision criteria:
- 1. First measure performance to confirm if there's a real problem
- 2. Whether components render frequently but props rarely change
- 3. Whether computations are complex or handle large data
- 4. Whether functions are passed to memo components or used as dependencies for other hooks
// Practical performance measurement method
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const start = performance.now();
return () => {
const end = performance.now();
console.log(`Render time: ${end - start}ms`);
};
});
// Component content...
}