Most React performance advice skips a step. Before you reach for memoization, measure where the time actually goes. Optimizing without a measurement is guessing, and a guessed optimization often adds complexity without making the UI any faster.
Measure the slow interaction first
A slow interaction always has a specific cause. Open the Profiler tab in React DevTools, record the interaction, and look for components that render more often than they should. The Profiler lists every component that rendered and how long each one took, so the real bottleneck stands out instead of leaving you to guess.
- Record one interaction at a time instead of a long session.
- Toggle Highlight updates to watch components re-render live.
- Compare commit durations, not just the total recorded time.
When one component appears in nearly every commit while its props never change, that is a re-render problem worth fixing. For the full recording and reading workflow, see how to find unnecessary re-renders with React Profiler.
This loop keeps every change tied to a measurement. Record once to find the cause, apply the smallest structural fix, then record the same interaction again to confirm it actually got faster.
Fix state placement before adding memo
Many unnecessary re-renders come from state living too high in the tree. When state changes, React re-renders the component that owns it and its entire subtree. A text input that shares a parent with a large list forces the list to re-render on every keystroke, even when the list data did not change.
import { useState } from "react";
export default function App({ results }) {
const [query, setQuery] = useState("");
return (
<>
<input value={query} onChange={(event) => setQuery(event.target.value)} />
<ResultList results={results} />
</>
);
}Here App owns the query state, so typing re-renders App and everything inside it. The ResultList component renders again on every keystroke even though the results prop never changed. The fix is to move the input and its state into their own component so the list stops being a child of the component that re-renders.
import { useState } from "react";
export default function App({ results }) {
return (
<>
<SearchField />
<ResultList results={results} />
</>
);
}
function SearchField() {
const [query, setQuery] = useState("");
return (
<input value={query} onChange={(event) => setQuery(event.target.value)} />
);
}Now typing re-renders only SearchField. ResultList is a child of App, and App no longer re-renders when the query changes, so the list stays untouched. This removes the re-render without any memoization.
Let children break the re-render chain
A component that wraps other UI often keeps its own state, such as a hover flag. When that state changes, the wrapper re-renders. If the expensive content arrives as the children prop instead of being rendered inside the wrapper, the children keep their identity and skip the re-render.
import { useState } from "react";
export default function Card({ children }) {
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={hovered ? "card hovered" : "card"}
>
{children}
</div>
);
}When the pointer enters the card, only Card re-renders. The children element was created by the parent and is the same object as before, so React does not re-render the expensive content inside it. Passing JSX as children is often the simplest way to stop a re-render before memoization is even needed.
Memoize only the measured bottleneck
When a structural fix is not possible, memoization can stop one specific re-render. Wrap a component in memo to skip rendering when its props are unchanged, and cache a value or function only when it is genuinely expensive or must keep a stable identity.
import { memo } from "react";
const ResultList = memo(function ResultList({ results }) {
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
});This only helps when ResultList receives the same props across renders. It does nothing if a parent recreates an array or function on every render, which is why memo needs stable props to work. Reserve useMemo and useCallback for the few places where a value or function must stay identical across renders.
Let React Compiler automate the rest
React Compiler applies this kind of memoization automatically at build time. It is stable, works with plain JavaScript and JSX, and can often replace manual memo calls instead of adding more. When it is enabled, the compiler handles memoization for components and hooks, which is the long-term alternative to guessing.
A decision checklist
- Measure first and confirm a re-render is the problem.
- Move state down to the component that needs it.
- Pass JSX as children so wrappers can re-render alone.
- Add memo only where the Profiler shows repeated, unchanged renders.
- Consider React Compiler to automate memoization across the app.
Rune AI
Key Insights
- Measure with React DevTools before changing any code.
- Move state down to the component that actually needs it.
- Pass JSX as children so wrappers re-render alone.
- Add memo only for a measured re-render bottleneck.
- React Compiler can automate memoization for you.
Frequently Asked Questions
Should I add useMemo and useCallback everywhere to be safe?
Why does moving state down improve performance?
Conclusion
Fast React apps come from measured fixes, not from sprinkling memoization everywhere. Find the slow interaction in the Profiler, fix state placement and composition first, then add memo only where the profiler proves it pays off.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.