Back

How to Improve Performance of a React App

Published - May 16, 2026

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:

Questions to ask:

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:

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:

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:

help render only items inside the viewport.

Why this matters

Without virtualization:

With virtualization:

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:

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:

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:

Good solutions:

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:

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:

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:

If needed:

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:

Ask yourself:

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:

Helpful strategies:

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:

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:

  1. measure the issue
  2. identify the bottleneck
  3. apply the smallest useful change
  4. verify improvement
  5. 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:

When performance work is guided by profiling instead of guesswork, React apps become noticeably faster without becoming harder to maintain.