Every "React performance" article promises exotic fixes — custom reconciliation, obscure APIs, virtualization libraries you've never heard of. In practice, the performance issues that actually show up in production code review are the same handful of patterns, over and over, across completely different teams and codebases.
1. Inline objects and functions passed as props
// Every render creates a new object and a new function —
// any memoized child re-renders anyway.
<UserCard
style={{ margin: 8 }}
onClick={() => handleClick(user.id)}
/>
React.memo on UserCard does nothing here, because style and onClick are new references on every render regardless of whether anything relevant changed. Move the object outside the render (or into useMemo), and wrap the handler in useCallback if it's actually passed to a memoized child.
This one is everywhere because it's invisible — the code looks completely normal, and nothing breaks. It just quietly re-renders more than it needs to.
2. Reaching for useEffect to compute derived state
// Unnecessary effect, extra render, and a state variable
// that can go out of sync with its source.
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
If a value can be computed directly from existing state or props, compute it during render:
const fullName = `${firstName} ${lastName}`;
No effect, no extra state, no extra render, and no chance of fullName drifting out of sync with the values it's derived from. useEffect is for synchronizing with something outside React — not for keeping two pieces of React state in sync with each other.
3. Rendering large lists without virtualization
A list of 20 items doesn't need virtualization. A list of 2,000 rendered in full — even with clean, memoized components — will visibly stutter on scroll, because the DOM node count alone becomes the bottleneck, independent of how efficient the React-level rendering is.
The fix isn't exotic: react-window or @tanstack/react-virtual for anything with an unbounded or large item count. The mistake is usually not knowing you've crossed the threshold until a real user's data — not your test fixture of 12 items — hits production.
4. Fetching waterfalls instead of parallel requests
// Sequential — each request waits for the previous one to finish.
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
If posts doesn't actually depend on the result of fetchUser beyond needing the id you already have, there's no reason for these to be sequential. Parallelize with Promise.all wherever the data doesn't have a genuine dependency chain. This shows up constantly in components that grew organically — each new data need bolted on as another sequential await instead of being reconsidered as a group.
How to actually find these instead of guessing
Reading code for these patterns catches maybe half of what's actually happening. The React DevTools Profiler tab shows you, concretely, which components re-rendered on a given interaction and why — "why did this render" is a real, clickable answer, not a guess. Turn it on before you optimize anything. Optimizing a component that wasn't actually a bottleneck is wasted work dressed up as productivity.
None of these four fixes are clever. That's the point — the vast majority of real-world React performance work is applying boring, well-understood fixes consistently, not discovering a trick nobody else knows.