DevelopmentMay 1, 2025

React Performance Optimization Techniques

Why React Performance Optimization Matters

React is fast by default, but as your application scales, it can slow down due to unnecessary renders or expensive calculations. Performance bottlenecks often creep in silently especially with large component trees or frequent re-renders.

That's where React's optimization tools come in:

  • React.memo

  • useCallback

  • useMemo

These tools help you control when and how your components re-render. But misusing them can do more harm than good.

In this guide, we’ll dive deep into all three, with clear examples and real-world analogies to help you make your React apps lightning-fast.

Understanding Re-renders in React (Before Optimization)

Before diving into optimization, it’s crucial to understand what triggers a re-render in React.

React re-renders a component when:

Its state or props change.

Its parent component re-renders.

Let’s look at an example:


function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Click</button>
<Child />
</>
);
}
function Child() {
console.log('Child rendered');
return <div>I’m the child</div>;
}

Every time the button is clicked, the parent re-renders—even though Child doesn't depend on count.

Result? Child logs "Child rendered" every time. That’s unnecessary. This is where React.memo can help.

React.memo: Prevent Unnecessary Component Re-renders

What is React.memo?

React.memo is a higher-order component that memoizes a component. It prevents the component from re-rendering unless its props change.

Syntax:


const MemoizedComponent = React.memo(MyComponent);

Real Example:


const Child = React.memo(({ name }) => {
console.log('Child rendered');
return <div>Hello, {name}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Click</button>
<Child name="React" />
</>
);
}

Now when the parent re-renders, Child will not—because its name prop hasn’t changed.

When to Use:

Use when your component is pure (output only depends on props).

Ideal for functional stateless components.

Great for lists, UI widgets, and static data displays.

Caution: Don’t blindly wrap everything with React.memo. It comes with memory overhead and may hurt performance if the props are complex or constantly changing.

useCallback: Memoize Functions Passed as Props

What Problem Does It Solve? In React, functions are re created on every render. So even if props look the same, a function prop may appear "new", triggering re-renders.

Example Without useCallback:


function Parent() {
const [count, setCount] = useState(0);
const sayHello = () => {
console.log('Hello');
};
return (
<>
<button onClick={() => setCount(count + 1)}>Click</button>
<Child onClick={sayHello} />
</>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Say Hello</button>;
});

Here, Child still re-renders because sayHello is re-created on every render. So the prop is “new”.

Fix With useCallback:


const sayHello = useCallback(() => {
console.log('Hello');
}, []);

Now the function reference remains the same between renders unless dependencies change.

Syntax:


const memoizedFn = useCallback(() => {
// do something
}, [dependencies]);

When to Use: When passing functions to memoized child components.

In event handlers inside large re-rendering components.

Inside dependencies of useEffect, useMemo.

useMemo: Memoize Expensive Calculations

Why Use useMemo? If you have a slow or expensive calculation, React will re-run it on every render by default—even if the input hasn’t changed.

Slow Example:


function slowFunction(num) {
console.log('Calculating...');
for (let i = 0; i < 1e9; i++) {} // pretend this takes time
return num \* 2;
}
function App() {
const [count, setCount] = useState(0);
const [value, setValue] = useState('');
const result = slowFunction(count);
return (
<>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<div>Result: {result}</div>
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}

Here, typing in the input field feels laggy, because slowFunction runs on every keystroke.

Fix With useMemo:


const result = useMemo(() => slowFunction(count), [count]);

Now the calculation only runs when count changes.

Syntax:


const memoizedValue = useMemo(() => computeSomething(a, b), [a, b]);

When to Use: For CPU-intensive or repeated calculations.

When values are derived from props or state.

Avoid premature optimization—measure first.

All Three Together: Practical Example

Let’s build a practical app using all three tools:

Task: A counter with:

Expensive calculation on count

A child component showing a memoized button

Prevent unnecessary re-renders


function slowCount(num) {
console.log('Slow count running...');
for (let i = 0; i < 1e9; i++) {}
return num \* 2;
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Say Hello</button>;
});
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const double = useMemo(() => slowCount(count), [count]);
const sayHello = useCallback(() => {
console.log('Hello!');
}, []);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<h2>Double Count: {double}</h2>
<button onClick={() => setCount(count + 1)}>+</button>
<Child onClick={sayHello} />
</>
);
}

Now:

Typing doesn’t trigger slowCount

Child button doesn’t re-render unless sayHello changes

Performance Tips and Best Practices

Do:

Use React.memo for presentational components

Use useCallback when passing functions down to memoized components

Use useMemo for heavy computations, not simple math

Don’t:

Over-optimize too early

Wrap everything in useMemo/useCallback without profiling

Use these hooks without understanding dependency arrays

Measuring Performance

Use these tools to spot bottlenecks:

React DevTools Profiler

Chrome Performance Tab

why-did-you-render (debugging tool to detect unnecessary re-renders)


npm install @welldone-software/why-did-you-render

In your index.js:


import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}