React Performance Optimization Guide


I used to wrap every component in React.memo because someone on Twitter said it was "free performance." It was not free. It was harder-to-read code and sometimes slower renders from shallow compare overhead.
This guide is what I do now: Profiler first, optimize the hot path, ignore the rest. Pair with React 19 features when you are choosing patterns for forms and optimistic UI.
What: React DevTools Profiler — record an interaction, see what re-rendered and how long it took.
Why: Guessing wastes time. One heavy parent re-rendering fifty children beats one unmemoized leaf.
Red flag: Optimizing before you know the bottleneck. Seriously. Profile.
What: Skip re-render if props are shallow-equal to last time.
import React, { memo, useState } from "react";
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log("ExpensiveList render");
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
);
});
export default function App() {
const [count, setCount] = useState(0);
const items = [{ id: 1, label: "Static" }];
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<ExpensiveList items={items} />
</div>
);
}
When I use it: Expensive child, stable props, parent re-renders often for unrelated state.
When I skip it: Cheap components, props that change every render anyway, premature optimization.
What: Return the same function identity until dependencies change.
import React, { useState, useCallback, memo } from "react";
const Child = memo(function Child({ onClick }) {
console.log("Child render");
return <button onClick={onClick}>Click</button>;
});
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Inc: {count}</button>
<Child onClick={handleClick} />
</div>
);
}
Why: Without useCallback, Child gets a new onClick every parent render → memo useless.
Red flag: useCallback on every handler "just in case." Noise without a memoized child consuming it.
What: Cache a computed value between renders.
const sorted = useMemo(() => heavySort(items), [items]);
When: Sorting/filtering large lists, heavy formatting, referential equality for deps of other hooks.
When not: Trivial math — the memo cost can exceed the work.
React.lazy + SuspenseuseDeferredValue, startTransition in React 18+After you record an interaction, look for:
I screenshot the before state. Future me forgets what "felt slow."
style={{ color: 'red' }} and inline {} defeat memo every time.Open Profiler. Click the slow interaction. Fix the top offender — usually state placement or a missing key, not missing memo.
React performance is mostly architecture and data flow. memo, useCallback, and useMemo are precision tools for proven hot paths. Use them with evidence, not superstition.