Performance and memo
React re-renders a component when its state changes, and with it all its children, even if their props did not change. Usually this is fast and unnoticeable. But if a child is heavy, for example a list of a thousand rows, extra re-renders start to slow typing down. The memo function lets React skip re-rendering a component when its props stay the same.
When a component re-renders
| Reason | Example |
|---|---|
| Its state changed | setText was called in this component |
| Its parent re-rendered | Any state in App changed |
| A context it reads changed | The theme was switched |
A change of props is not a reason by itself: props change only because the parent re-rendered.
Finding the slow spot
Install the React Developer Tools browser extension. On the Profiler tab start recording, do the slow action — for example, type into a field — and stop recording. The Profiler shows which components re-rendered and how long it took. Optimize only what is actually slow.
An example problem
In the to-do list, the input and the list live in one component. Every keystroke changes text and re-renders the whole TodoList:
// src/App.jsx
export default function App() {
const [text, setText] = useState('');
const [todos, setTodos] = useState(initialTodos);
return (
<>
<input value={text} onChange={(event) => setText(event.target.value)} />
<TodoList todos={todos} onDelete={handleDelete} />
</>
);
}With a dozen tasks it does not matter. With thousands, typing starts to lag.
memo
Wrap the component in memo — in src/components/TodoList.jsx:
// src/components/TodoList.jsx
import { memo } from 'react';
import TodoItem from './TodoItem.jsx';
const TodoList = memo(function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onDelete={onDelete} />
))}
</ul>
);
});
export default TodoList;Now React compares the new props with the old ones and, if they are all equal, skips the re-render. Typing in the field no longer touches the list.
memo and functions in props
Props are compared strictly, with ===. The handleDelete function declared in App is created anew on every render, and memo sees a new prop. That is why it is remembered with useCallback — see the lesson on useMemo and useCallback. The same applies to objects and arrays created right in the markup.
Other approaches
Often a simple rearrangement solves the problem: move the input into its own AddTodoForm component with its own state — then typing re-renders only the form and does not touch App or the list. This is better than memo because no props have to be compared. Very long lists use virtualization — rendering only the visible rows, for example with the react-window library.
Common mistake
memo does not help if a prop gets a new object or function every time:
// Wrong: style and onDelete are new on every render of App.
<TodoList
todos={todos}
style={{ margin: 8 }}
onDelete={(id) => handleDelete(id)}
/>Move constant objects outside the component and wrap functions in useCallback. Otherwise memo only adds the extra work of comparing props.