Avoiding Unnecessary Re-Renders in React: Complete Guide
Re-renders are a fundamental part of React's reactivity model—they detect what changed and update the DOM accordingly. However, not all re-renders are necessary. When your application grows, unnecessary re-renders compound into noticeable performance problems: laggy input fields, stuttering animations, and frozen UI during heavy computations. This guide teaches you exactly when re-renders happen and how to prevent the unnecessary ones without over-optimizing.
Table of Contents
- Understanding React's Render Cycle
- When Re-Renders Happen
- React.memo: Preventing Child Re-Renders
- useMemo: Memoizing Expensive Computations
- useCallback: Stabilizing Function References
- Real-World Optimization Scenarios
- Common Performance Pitfalls
- React Compiler: Automatic Optimization
- FAQ
Understanding React's Render Cycle
Before optimizing, you need to understand what "render" actually means in React. This is where many developers get confused.
What is a "Render"?
A render is when React calls your component function to determine what the UI should look like. It's not the same as updating the DOM—that's a separate step. Here's the cycle:
1. State/Props Change
↓
2. React Calls Component Function (this is "rendering")
↓
3. React Compares Old and New Virtual DOM (reconciliation)
↓
4. React Updates Actual DOM (only if needed)
Key insight: Step 2 happens much more often than Step 4. In a typical application with optimizations, a component might render 50 times but only update the DOM 5 times. This is by design—React is very fast at rendering because the virtual DOM comparison is lightweight.
Virtual DOM Reconciliation
React uses a clever algorithm to determine if the actual DOM needs updating:
// ✅ Same structure, same values = no DOM update needed
<div>Hello {name}</div>
// Renders again with same name = no DOM change
// ❌ Different structure or different values = DOM updates
<div>Hello {name}</div>
// Renders with different name = DOM updates
This reconciliation happens automatically. Re-renders are only a problem when:
- The component is expensive to compute
- The re-render triggers expensive child component re-renders
- You're doing side effects inside the render function (which you shouldn't)
When Re-Renders Happen
Understanding exactly when React re-renders your components is crucial for optimization.
Rule 1: Component Re-Renders When Its State Changes
function Counter() {
const [count, setCount] = useState(0);
// This component re-renders every time count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Rule 2: Component Re-Renders When Its Props Change
interface CardProps {
title: string;
isActive: boolean;
}
function Card({ title, isActive }: CardProps) {
// Re-renders whenever title or isActive changes
return (
<div className={isActive ? 'active' : ''}>
<h2>{title}</h2>
</div>
);
}
Rule 3: Child Components Re-Render When Parent Re-Renders (Unless Optimized)
This is the most common source of unnecessary re-renders:
TypeScript Version
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* ExpensiveChild WILL re-render when Parent re-renders,
even though ExpensiveChild receives no props! */}
<ExpensiveChild />
</div>
);
}
function ExpensiveChild() {
console.log('ExpensiveChild rendered'); // logs every time Parent renders
// Imagine this is complex computation
const result = heavyCalculation();
return <div>{result}</div>;
}
// Without optimization: Click increment → Parent renders → ExpensiveChild re-renders
// With optimization: Click increment → Parent renders → ExpensiveChild does NOT re-render
JavaScript Version
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ExpensiveChild />
</div>
);
}
function ExpensiveChild() {
console.log('ExpensiveChild rendered');
const result = heavyCalculation();
return <div>{result}</div>;
}
React.memo: Preventing Child Re-Renders
React.memo is a higher-order component that memoizes a component. It prevents re-rendering unless the component's props actually change.
Basic Usage
TypeScript Version
import { memo, useState, ReactNode } from 'react';
interface StatusBadgeProps {
status: 'active' | 'inactive' | 'pending';
count: number;
}
// Wrap component with memo
const StatusBadge = memo(function StatusBadge({ status, count }: StatusBadgeProps) {
console.log('StatusBadge rendered');
return (
<div className={`badge badge-${status}`}>
<span>{status}</span>
<span className="count">{count}</span>
</div>
);
});
function Dashboard() {
const [filter, setFilter] = useState('');
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
{/* StatusBadge only re-renders if status or count props change,
NOT just because Dashboard re-rendered */}
<StatusBadge status="active" count={5} />
<StatusBadge status="inactive" count={2} />
</div>
);
}
JavaScript Version
import { memo, useState } from 'react';
const StatusBadge = memo(function StatusBadge({ status, count }) {
console.log('StatusBadge rendered');
return (
<div className={`badge badge-${status}`}>
<span>{status}</span>
<span className="count">{count}</span>
</div>
);
});
function Dashboard() {
const [filter, setFilter] = useState('');
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<StatusBadge status="active" count={5} />
<StatusBadge status="inactive" count={2} />
</div>
);
}
Custom Comparison Logic
By default, React.memo uses shallow comparison. For complex objects, you might need custom comparison:
interface UserProps {
user: {
id: string;
name: string;
email: string;
};
}
const UserCard = memo(
({ user }: UserProps) => {
return <div>{user.name}</div>;
},
// Custom comparison function
(prevProps, nextProps) => {
// Return true if props are equal (don't re-render)
// Return false if props are different (do re-render)
return prevProps.user.id === nextProps.user.id;
}
);
Common Mistake: Passing New Objects to Memoized Components
// ❌ PROBLEM: memo is useless here
function Parent() {
return (
<MemoizedChild
config={{ theme: 'dark', size: 'large' }}
/>
);
}
const MemoizedChild = memo(({ config }) => {
// Re-renders every time because { theme: 'dark', size: 'large' }
// creates a new object in memory on each render
return <div>{config.theme}</div>;
});
// ✅ SOLUTION: Move object outside or memoize it
const CONFIG = { theme: 'dark', size: 'large' }; // constant
function Parent() {
return <MemoizedChild config={CONFIG} />;
}
useMemo: Memoizing Expensive Computations
useMemo caches the result of expensive computations and only recalculates when dependencies change.
When to Use useMemo
Use useMemo for expensive operations that happen during render. Common cases:
- Sorting or filtering large lists
- Complex calculations
- Creating new objects/arrays that are passed as props
TypeScript Version
import { useMemo, useState } from 'react';
interface User {
id: string;
name: string;
score: number;
}
interface UserListProps {
users: User[];
sortBy: 'name' | 'score';
}
function UserList({ users, sortBy }: UserListProps) {
// Without useMemo: sorting happens on every render
// With useMemo: sorting only happens when users or sortBy changes
const sortedUsers = useMemo(() => {
console.log('Sorting users...');
const sorted = [...users].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return b.score - a.score;
});
return sorted;
}, [users, sortBy]); // Re-sort only when these dependencies change
return (
<ul>
{sortedUsers.map((user) => (
<li key={user.id}>
{user.name} - Score: {user.score}
</li>
))}
</ul>
);
}
JavaScript Version
import { useMemo, useState } from 'react';
function UserList({ users, sortBy }) {
const sortedUsers = useMemo(() => {
console.log('Sorting users...');
const sorted = [...users].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return b.score - a.score;
});
return sorted;
}, [users, sortBy]);
return (
<ul>
{sortedUsers.map((user) => (
<li key={user.id}>
{user.name} - Score: {user.score}
</li>
))}
</ul>
);
}
Performance Cost of useMemo
Here's the key insight: useMemo has overhead. React must:
- Run the dependency comparison
- Store the memoized value in memory
- Check equality on each render
Use useMemo only when:
- The computation is expensive (sorts, filters, complex calculations)
- The component re-renders frequently
- You've measured that it helps
Don't use useMemo for:
- Simple operations (variable assignments, simple lookups)
- Objects that aren't passed as props to memoized children
- "Just to be safe"—profile first
useCallback: Stabilizing Function References
useCallback memoizes function references. It's primarily useful for preventing unnecessary re-renders when passing callbacks to memoized child components.
The Function Recreation Problem
TypeScript Version
import { memo, useCallback, useState } from 'react';
interface DeleteButtonProps {
onDelete: () => void;
itemId: string;
}
// Without optimization, DeleteButton re-renders whenever
// Parent re-renders, even though the button does nothing different
const DeleteButton = memo(function DeleteButton({ onDelete, itemId }: DeleteButtonProps) {
console.log(`DeleteButton ${itemId} rendered`);
return (
<button onClick={onDelete} className="btn-danger">
Delete {itemId}
</button>
);
});
function ItemList() {
const [items, setItems] = useState(['item-1', 'item-2', 'item-3']);
const [filter, setFilter] = useState('');
// ❌ PROBLEM: New function created on every render
// Even though the logic is the same, it's a different function object
const handleDelete = (id: string) => {
setItems((prev) => prev.filter((item) => item !== id));
};
// ✅ SOLUTION: Memoize the function with useCallback
const handleDeleteMemo = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item !== id));
}, []); // Empty deps because callback doesn't depend on anything
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
{items
.filter((item) => item.includes(filter))
.map((item) => (
<DeleteButton
key={item}
itemId={item}
onDelete={() => handleDeleteMemo(item)}
/>
))}
</div>
);
}
JavaScript Version
import { memo, useCallback, useState } from 'react';
const DeleteButton = memo(function DeleteButton({ onDelete, itemId }) {
console.log(`DeleteButton ${itemId} rendered`);
return (
<button onClick={onDelete} className="btn-danger">
Delete {itemId}
</button>
);
});
function ItemList() {
const [items, setItems] = useState(['item-1', 'item-2', 'item-3']);
const [filter, setFilter] = useState('');
// With useCallback, the function reference stays the same
// across renders (unless dependencies change)
const handleDeleteMemo = useCallback((id) => {
setItems((prev) => prev.filter((item) => item !== id));
}, []);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
{items
.filter((item) => item.includes(filter))
.map((item) => (
<DeleteButton
key={item}
itemId={item}
onDelete={() => handleDeleteMemo(item)}
/>
))}
</div>
);
}
When useCallback is Actually Useful
useCallback only helps when:
- You're passing the callback to a memoized child component
- The child component actually uses
React.memoor equivalent - Without
useCallback, the child would re-render frequently
// ❌ useCallback doesn't help here - SomeChild is NOT memoized
function Parent() {
const callback = useCallback(() => {}, []);
return <SomeChild onAction={callback} />;
}
function SomeChild({ onAction }) {
// Re-renders anyway because Parent re-renders!
return <button onClick={onAction}>Click</button>;
}
// ✅ useCallback helps here - SomeChild IS memoized
const MemoizedChild = memo(SomeChild);
function Parent() {
const callback = useCallback(() => {}, []);
// MemoizedChild only re-renders if callback reference changes
return <MemoizedChild onAction={callback} />;
}
Real-World Optimization Scenarios
Scenario 1: Large List with Filter and Sort
A common pattern: displaying thousands of items with filtering and sorting. This is where optimization matters most.
TypeScript Version
import { useMemo, useState, memo, useCallback } from 'react';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface ProductRowProps {
product: Product;
onSelect: (id: string) => void;
}
const ProductRow = memo(function ProductRow({ product, onSelect }: ProductRowProps) {
return (
<tr onClick={() => onSelect(product.id)}>
<td>{product.name}</td>
<td>${product.price}</td>
<td>{product.category}</td>
</tr>
);
});
function ProductCatalog({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
const [selectedId, setSelectedId] = useState<string | null>(null);
// Filter and sort products only when dependencies change
const filteredAndSortedProducts = useMemo(() => {
console.log('Filtering and sorting...');
let result = products.filter((p) =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.category.toLowerCase().includes(searchTerm.toLowerCase())
);
result = result.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
return result;
}, [products, searchTerm, sortBy]);
// Memoize callback so ProductRow doesn't re-render unnecessarily
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search products..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
<table>
<tbody>
{filteredAndSortedProducts.map((product) => (
<ProductRow
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</tbody>
</table>
</div>
);
}
JavaScript Version
import { useMemo, useState, memo, useCallback } from 'react';
const ProductRow = memo(function ProductRow({ product, onSelect }) {
return (
<tr onClick={() => onSelect(product.id)}>
<td>{product.name}</td>
<td>${product.price}</td>
<td>{product.category}</td>
</tr>
);
});
function ProductCatalog({ products }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('name');
const [selectedId, setSelectedId] = useState(null);
const filteredAndSortedProducts = useMemo(() => {
console.log('Filtering and sorting...');
let result = products.filter((p) =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.category.toLowerCase().includes(searchTerm.toLowerCase())
);
result = result.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
return result;
}, [products, searchTerm, sortBy]);
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search products..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
<table>
<tbody>
{filteredAndSortedProducts.map((product) => (
<ProductRow
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</tbody>
</table>
</div>
);
}
Scenario 2: Complex Form with Validation
Forms are where unnecessary re-renders really hurt—every keystroke causes re-renders.
TypeScript Version
import { useState, useCallback, useMemo, memo } from 'react';
interface FormErrors {
[key: string]: string;
}
interface FormData {
email: string;
password: string;
confirmPassword: string;
}
const FormInput = memo(function FormInput({
name,
value,
onChange,
error,
}: {
name: string;
value: string;
onChange: (value: string) => void;
error?: string;
}) {
console.log(`FormInput ${name} rendered`);
return (
<div>
<input
type={name === 'password' ? 'password' : 'text'}
value={value}
onChange={(e) => onChange(e.target.value)}
className={error ? 'error' : ''}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
});
function SignUpForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
confirmPassword: '',
});
// Validate only when formData changes, not on every render
const errors = useMemo(() => {
const newErrors: FormErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
}, [formData]);
// Memoize handlers so FormInput doesn't re-render unnecessarily
const handleEmailChange = useCallback((value: string) => {
setFormData((prev) => ({ ...prev, email: value }));
}, []);
const handlePasswordChange = useCallback((value: string) => {
setFormData((prev) => ({ ...prev, password: value }));
}, []);
const handleConfirmPasswordChange = useCallback((value: string) => {
setFormData((prev) => ({ ...prev, confirmPassword: value }));
}, []);
const isValid = Object.keys(errors).length === 0;
return (
<form onSubmit={(e) => {
e.preventDefault();
if (isValid) {
console.log('Submit:', formData);
}
}}>
<FormInput
name="email"
value={formData.email}
onChange={handleEmailChange}
error={errors.email}
/>
<FormInput
name="password"
value={formData.password}
onChange={handlePasswordChange}
error={errors.password}
/>
<FormInput
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleConfirmPasswordChange}
error={errors.confirmPassword}
/>
<button type="submit" disabled={!isValid}>
Sign Up
</button>
</form>
);
}
JavaScript Version
import { useState, useCallback, useMemo, memo } from 'react';
const FormInput = memo(function FormInput({
name,
value,
onChange,
error,
}) {
console.log(`FormInput ${name} rendered`);
return (
<div>
<input
type={name === 'password' ? 'password' : 'text'}
value={value}
onChange={(e) => onChange(e.target.value)}
className={error ? 'error' : ''}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
});
function SignUpForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: '',
});
const errors = useMemo(() => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
}, [formData]);
const handleEmailChange = useCallback((value) => {
setFormData((prev) => ({ ...prev, email: value }));
}, []);
const handlePasswordChange = useCallback((value) => {
setFormData((prev) => ({ ...prev, password: value }));
}, []);
const handleConfirmPasswordChange = useCallback((value) => {
setFormData((prev) => ({ ...prev, confirmPassword: value }));
}, []);
const isValid = Object.keys(errors).length === 0;
return (
<form onSubmit={(e) => {
e.preventDefault();
if (isValid) console.log('Submit:', formData);
}}>
<FormInput
name="email"
value={formData.email}
onChange={handleEmailChange}
error={errors.email}
/>
<FormInput
name="password"
value={formData.password}
onChange={handlePasswordChange}
error={errors.password}
/>
<FormInput
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleConfirmPasswordChange}
error={errors.confirmPassword}
/>
<button type="submit" disabled={!isValid}>
Sign Up
</button>
</form>
);
}
Common Performance Pitfalls
Pitfall 1: Memoizing Too Early
// ❌ OVER-OPTIMIZATION: Component rarely re-renders
const SimpleButton = memo(function SimpleButton({ onClick }) {
return <button onClick={onClick}>Click</button>;
});
// ✅ BETTER: Only memoize if it actually helps
function SimpleButton({ onClick }) {
return <button onClick={onClick}>Click</button>;
}
Pitfall 2: useMemo with Shallow Dependencies
// ❌ PROBLEM: useMemo does more work than the computation
const items = useMemo(() => {
return array.map((x) => x * 2); // Simple operation
}, [array]);
// ✅ BETTER: Just do the computation
const items = array.map((x) => x * 2);
Pitfall 3: useCallback Without Memoized Children
// ❌ POINTLESS: Child isn't memoized
function Parent() {
const callback = useCallback(() => {}, []); // useless
return <Child onAction={callback} />;
}
function Child({ onAction }) {
return <button onClick={onAction}>Click</button>;
}
// ✅ CORRECT: Child is memoized
const MemoizedChild = memo(Child);
function Parent() {
const callback = useCallback(() => {}, []);
return <MemoizedChild onAction={callback} />;
}
Pitfall 4: Missing Dependencies
// ❌ BUG: count changes but isn't in dependencies
const handleClick = useCallback(() => {
console.log(count); // always logs 0!
}, []); // missing count dependency
// ✅ CORRECT: Include all dependencies
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
React Compiler: Automatic Optimization
React 19 introduced an experimental compiler that automatically optimizes your code. It injects React.memo, useMemo, and useCallback where beneficial.
Setting Up the Compiler
npm install babel-plugin-react-compiler
vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', {}],
],
},
}),
],
});
With the compiler enabled, you write normal React code, and it optimizes automatically:
// You write this (no manual memoization)
function ProductList({ products, onSelect }) {
const filtered = products.filter(p => p.active);
return (
<div>
{filtered.map(p => (
<ProductRow key={p.id} product={p} onSelect={onSelect} />
))}
</div>
);
}
// Compiler transforms it to (approximately)
const ProductList = memo(function ProductList({ products, onSelect }) {
const filtered = useMemo(
() => products.filter(p => p.active),
[products]
);
return (
<div>
{filtered.map(p => (
<ProductRow key={p.id} product={p} onSelect={onSelect} />
))}
</div>
);
});
Note: The compiler is experimental in React 19. Once stable, it will likely become standard.
Performance Monitoring in Production
Using React DevTools Profiler
React DevTools includes a powerful Profiler that shows exactly which components render and how long each takes:
// In development, wrap your app with Profiler to measure performance
import { Profiler } from 'react';
function App() {
const onRenderCallback = (
id, // component identifier
phase, // "mount" or "update"
actualDuration, // time spent rendering (ms)
baseDuration, // unoptimized render time
startTime,
commitTime,
) => {
console.log(`${id} (${phase}): ${actualDuration}ms`);
};
return (
<Profiler id="app" onRender={onRenderCallback}>
<App />
</Profiler>
);
}
Steps to use the Profiler:
- Open React DevTools → Profiler tab
- Click "Record" and interact with your application
- Stop recording
- Examine the flame graph showing which components took longest
- Look for unexpected re-renders or slow components
Identifying Real Performance Problems
A render taking 5ms is not a problem. Here's what matters:
| Metric | Status | Action |
|---|---|---|
| Render time < 16ms | ✅ Good | No action needed |
| Render time 16-50ms | ⚠️ Watch | Profile to understand cause |
| Render time > 50ms | 🔴 Problem | Optimize immediately |
| Input lag visible | 🔴 Problem | Major optimization needed |
Real problem indicators:
- User sees stuttering when typing in inputs
- Animations feel choppy (< 60fps)
- Large lists are slow to scroll
- Opening modals/dropdowns has visible delay
Enterprise Application Optimization
Large-scale applications (like those at Alibaba, ByteDance) use a sophisticated approach:
// 1. Strategic memoization at boundary components
const MemoizedPage = memo(Page); // Pages rarely need full re-renders
// 2. Separate fast and slow concerns
function MainDashboard() {
return (
<div>
{/* Fast: updates frequently, simple render */}
<LiveMetrics />
{/* Slow: expensive computation, memoized */}
<MemoizedAnalyticsChart data={data} />
{/* Fast: doesn't need all data */}
<UserMenu />
</div>
);
}
// 3. Code split expensive components
const HeavyReport = lazy(() => import('./HeavyReport'));
function Dashboard() {
const [showReport, setShowReport] = useState(false);
return (
<div>
<button onClick={() => setShowReport(true)}>Show Report</button>
{showReport && (
<Suspense fallback={<Spinner />}>
<HeavyReport />
</Suspense>
)}
</div>
);
}
// 4. Virtualize long lists
import { FixedSizeList } from 'react-window';
function LargeList({ items }: { items: Item[] }) {
// Only render visible items - huge performance gain
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
FAQ
Q: How do I know if I have a re-render problem?
A: Use React DevTools Profiler. Record a user interaction and look at:
- Render times: Are slow components rendering too often?
- Unexpected renders: Is a component re-rendering when it shouldn't?
- Visual feedback: Does the app feel responsive? If yes, you don't have a problem.
Remember: The goal isn't perfect performance metrics—it's a responsive user experience. If the app feels smooth, optimization is complete.
Q: Should I memoize everything to be safe?
A: No. Memoization has overhead—dependency checking, memory storage, and comparison logic. Only memoize when profiling shows it helps. A good rule: optimize for perceived performance, not theoretical performance.
Q: Can memoization hurt performance?
A: Yes. If memoization overhead exceeds the cost of re-rendering, it's slower. This happens with very fast computations or complex dependency arrays. Always profile before and after optimizing.
Q: Why does my memoized component still re-render?
A: Common causes:
- Props changed (check what's being passed)
- Parent memoized nothing (child always re-renders when parent does)
- Object/array props created inline (always "new")
- Missing
useCallbackfor function props
Q: Is React.memo the same as useMemo?
A: No.
React.memo: Prevents re-rendering of a component based on propsuseMemo: Memoizes a value computed during render
Q: What's the performance difference between memo and not memoizing?
A: It depends:
- Without memo: Component re-renders → fast render check → decision to update DOM
- With memo: Component skips render entirely
For a component that renders in 1ms, memoization saves 1ms. For a component that renders in 0.1ms, memoization overhead might be worse. Profile!
Questions? Share your performance optimization challenges and discoveries in the comments. What techniques have you found most effective in your applications?
Google AdSense Placeholder
CONTENT SLOT