Runnable lesson · press Try it on the examples to edit and run them here.
React is fast by default. Most apps never need optimization. When something does feel slow, the way to fix it is a method, not a bag of tricks: find the problem, then apply the smallest fix, then measure again.
Step 1: understand why components render
A component renders when:
- its state changes,
- its parent renders (and, by default, all children render with it, even when their props did not change),
- a context it reads changes.
Rendering means calling the function and comparing the result. It is usually cheap. Only commits change the DOM, and React skips those when nothing differs.
Here is a parent that re-renders on every keystroke. Its child does unrelated work. Watch the render counter of the child:
function Child() {
const renders = React.useRef(0);
renders.current++;
return <p>I do not depend on the input. Child rendered {renders.current} time(s)</p>;
}
function App() {
const [name, setName] = React.useState("");
return (
<div>
<input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
<Child />
</div>
);
}
After typing three letters, Child has rendered 4 times: once at the start and once per keystroke, though its output never changed. (The render counter uses a ref only to make the demo visible; do not write render counters in real components.) For a small child this costs almost nothing.
Step 2: measure before fixing
Do not guess. Use:
- React Developer Tools, Profiler tab: record an interaction and see which components rendered, why and for how long. Turn on “Highlight updates when components render” to see renders as flashing boxes.
- The browser Performance panel to find long tasks.
<Profiler id onRender>, a React component that reports render timings from code.- Production builds for honest numbers. Development mode is much slower because of extra checks (Production Builds with Vite).
If you cannot see a slowdown, you do not have a problem to fix.
Step 3: the fixes, from cheapest
1. Move state down. If only one small part needs the fast-changing state (like the text of an input), put that state in a small component of its own, so the rest of the tree does not re-render.
function HeavyList() {
const renders = React.useRef(0);
renders.current++;
return (
<div>
<p>Heavy list rendered {renders.current} time(s)</p>
<ul>{Array.from({ length: 5 }, (_, i) => <li key={i}>Row {i + 1}</li>)}</ul>
</div>
);
}
// The state is now local to this small component
function SearchBox() {
const [text, setText] = React.useState("");
return <input aria-label="Search" value={text} onChange={(e) => setText(e.target.value)} />;
}
function App() {
return (
<div>
<SearchBox />
<HeavyList />
</div>
);
}
Typing now re-renders only SearchBox; HeavyList renders once. Often this is all you need.
2. Pass children (composition). A parent that re-renders but receives its heavy part as children from above does not re-create it (Component Composition).
3. React.memo. Wrap a component so React skips re-rendering it when its props are shallowly equal to the last time:
const Expensive = React.memo(function Expensive({ label }) {
const renders = React.useRef(0);
renders.current++;
return <p>{label}. Expensive rendered {renders.current} time(s)</p>;
});
function App() {
const [name, setName] = React.useState("");
return (
<div>
<input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
<Expensive label="Always the same text" />
</div>
);
}
memo helps only when props are stable. A new object, array or function created in the parent each render breaks it. That is where useMemo and useCallback come in (useMemo, useCallback).
4. Keep expensive calculations out of render with useMemo, only when measured.
5. Keep the UI responsive during heavy updates with transitions (Keeping the UI Responsive).
6. Virtualize long lists. Rendering 10,000 rows is slow however clever your components are. Windowing libraries (such as TanStack Virtual or react-window) render only the rows that are visible. Pagination (Pagination) is the simple alternative.
7. Split code so users download less (Lazy Loading).
8. Avoid unnecessary effects and state. Every state update is a render; derive instead (Derived State).
The React Compiler
The React Compiler (version 1.0 since October 2025) memoizes automatically at build time for code that follows the Rules of React. If you use it, much manual memo, useMemo and useCallback becomes unnecessary. See React Compiler.
Common bad habits
- Wrapping everything in
memo,useMemoanduseCallback“just in case”. It adds complexity, and memoization has its own cost. - Optimizing in development mode. Measure a production build.
- Index keys and unstable keys, which cause remounting and lost state (Understanding React Keys).
- Giant components that render a lot for tiny changes: split them.
- Heavy work in render such as sorting 50,000 items on every key press.
Not only rendering
Often the slowest part of a page is not React: large images, blocking third-party scripts, slow API calls or a huge bundle. Check the Network and Performance panels before blaming rendering.
Key points
- Components render when state, parent or context changes; rendering is usually cheap.
- Measure with the Profiler on a production build, then apply the cheapest fix: move state down, use composition, then
memo. - Virtualize long lists and split code; do not memoize everything by default.