Performance work in React is not about randomly adding memo, useCallback, or useMemo everywhere. The real goal is to understand what is slow, why it is slow, and which change removes the bottleneck with the least complexity.
This article covers the most practical ways to improve the performance of a React application in real-world projects.
Start with measurement, not guesses
Before optimizing, find out what is actually causing the slowdown.
Useful tools:
- React DevTools Profiler
- browser Performance panel
- Lighthouse
- bundle analyzer
- network tab
Questions to ask:
- Is the page slow to load?
- Is the initial bundle too large?
- Are components re-rendering too often?
- Is a list too large to render?
- Are API calls blocking the UI?
- Is expensive logic running on every render?
If you skip measurement, you may spend time optimizing the wrong thing.
1. Reduce unnecessary re-renders
One of the most common React performance issues is re-rendering components that do not need to update.
Example problem
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child />
</>
);
}
function Child() {
console.log('Child rendered');
return <div>Static child</div>;
}Every time Parent updates, Child renders too.
Optimization with React.memo
const Child = React.memo(function Child() {
console.log('Child rendered');
return <div>Static child</div>;
});Now Child skips re-rendering if its props do not change.
Important note
Do not wrap every component in React.memo. It helps when:
- the component renders often
- props are stable
- the component is expensive enough to justify memoization
2. Keep state as local as possible
Lifting state too high can trigger large parts of the tree to re-render.
Less ideal
function App() {
const [inputValue, setInputValue] = useState('');
return (
<>
<Header />
<Sidebar />
<SearchBox value={inputValue} onChange={setInputValue} />
</>
);
}If inputValue only matters to SearchBox, storing it at the root creates unnecessary parent renders.
Better
function SearchBox() {
const [inputValue, setInputValue] = useState('');
return (
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
);
}Local state reduces render scope and keeps component ownership cleaner.
3. Avoid recreating expensive work on every render
If a calculation is heavy, do not run it unnecessarily.
Example
function ProductList({ products, filter }) {
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
return filteredProducts.map((product) => (
<div key={product.id}>{product.name}</div>
));
}If the filtering becomes expensive, memoize the derived result.
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return filteredProducts.map((product) => (
<div key={product.id}>{product.name}</div>
));
}Use useMemo only when it prevents meaningful repeated work. It is not a blanket rule.
4. Stabilize callbacks only when it matters
Passing a newly created function on every render can break memoization in child components.
Example
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <ExpensiveButton onClick={handleClick} count={count} />;
}This is useful when:
- the child is memoized
- the child depends on function identity stability
If not, useCallback may add noise without real performance gain.
5. Use list virtualization for large datasets
Rendering hundreds or thousands of items at once is expensive.
Instead of rendering everything, render only the visible portion.
Libraries like:
react-windowreact-virtualized- TanStack Virtual
help render only items inside the viewport.
Why this matters
Without virtualization:
- many DOM nodes are created
- scroll performance drops
- layout and paint costs increase
With virtualization:
- fewer DOM nodes exist at once
- scrolling feels smoother
- memory usage improves
6. Split code and lazy load heavy features
Large bundles slow down first load and time to interactive.
Example
const ReportsPage = React.lazy(() => import('./ReportsPage'));<Suspense fallback={<div>Loading...</div>}>
<ReportsPage />
</Suspense>Code splitting is great for:
- route-level pages
- admin dashboards
- chart libraries
- editors
- modals with heavy dependencies
Load expensive code only when the user actually needs it.
7. Debounce or defer frequent updates
Typing into a search field can trigger heavy filtering or network calls on every keystroke.
Debounce example
useEffect(() => {
const timer = setTimeout(() => {
fetchResults(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);React 18 tools
React also provides:
useDeferredValuestartTransition
These help keep urgent UI updates responsive while non-urgent updates happen in the background.
Example
const deferredQuery = useDeferredValue(query);This can make typing feel smoother when filtering large lists.
8. Prevent waterfall data fetching
Fetching data too late or in the wrong order can slow screens down even if rendering is fast.
Common issues:
- a parent fetch completes before the child starts fetching
- components fetch the same data repeatedly
- no caching layer is used
Good solutions:
- request data earlier
- cache server state with tools like TanStack Query
- deduplicate requests
- prefetch likely next screens
React performance is not only render performance. Data flow is a big part of it.
9. Keep component trees focused
Huge components often do too much:
- fetch data
- transform data
- manage forms
- render lists
- handle modals
- manage side effects
Breaking them into focused components improves maintainability and can make rendering more predictable.
That said, do not split components only for the sake of splitting them. The aim is better ownership and smaller update surfaces.
10. Optimize images and non-JavaScript assets
Sometimes a React app feels slow because the page is heavy, not because React itself is slow.
Check for:
- oversized images
- layout shifts from missing dimensions
- large icons or animations
- blocking fonts
- too much CSS
In Next.js, the Image component helps with responsive image loading, sizing, and optimization.
11. Avoid expensive work inside render
Rendering should stay lightweight.
Avoid:
- heavy loops
- deep object cloning
- repeated date formatting
- large sorting operations
- synchronous parsing of big datasets
If needed:
- precompute on the server
- memoize derived values
- move work outside render
- use a Web Worker for truly heavy computation
12. Use proper keys in lists
Bad keys can cause incorrect reconciliation and unnecessary DOM updates.
Bad
items.map((item, index) => <Row key={index} item={item} />)Better
items.map((item) => <Row key={item.id} item={item} />)Stable keys help React update only what actually changed.
13. Memoize context values carefully
Context is useful, but if its value changes often, every consumer can re-render.
Example
<AuthContext.Provider value={{ user, logout }}>
{children}
</AuthContext.Provider>This object is recreated every render.
Better
const contextValue = useMemo(() => ({ user, logout }), [user, logout]);
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>Also avoid putting rapidly changing values into broad global context when only a few components need them.
14. Remove unnecessary effects
Many React performance problems come from effects that:
- update state too often
- trigger extra renders
- fetch repeatedly
- duplicate derived logic
Ask yourself:
- can this be derived during render?
- can this move to an event handler?
- can this be computed once?
Less effect usage often leads to simpler and faster code.
15. Watch bundle size
A React app can render efficiently and still feel slow because it ships too much JavaScript.
Look for:
- large chart libraries
- date libraries
- icon packs
- duplicated dependencies
- unused polyfills
Helpful strategies:
- route-based code splitting
- importing only what you use
- replacing heavy libraries with lighter alternatives
- using bundle analysis tools
16. Use server rendering or static generation when appropriate
If the app is slow because the browser must do too much on first load, server-side rendering or static generation can improve perceived performance.
This helps users:
- see content sooner
- wait less for initial page display
- get better SEO at the same time
In frameworks like Next.js, choosing the right rendering strategy is often a major performance decision.
Practical optimization workflow
A good React performance workflow looks like this:
- measure the issue
- identify the bottleneck
- apply the smallest useful change
- verify improvement
- stop when the problem is solved
That last step matters. Over-optimizing can make the code harder to maintain without giving noticeable user benefit.
Common mistakes
1. Memoizing everything
Too much memoization makes code noisy and can even add overhead.
2. Blaming React for network or asset problems
Sometimes the issue is large images, slow APIs, or huge bundles.
3. Storing too much global state
This often increases render scope unnecessarily.
4. Ignoring user-perceived performance
A technically optimized app is not enough if loading still feels slow.
Final thoughts
Improving React performance is mostly about smarter rendering boundaries, lighter bundles, better data flow, and measuring before optimizing.
Focus on:
- fewer unnecessary re-renders
- smaller initial bundles
- lighter rendering work
- faster data delivery
- smoother user interactions
When performance work is guided by profiling instead of guesswork, React apps become noticeably faster without becoming harder to maintain.