Most React performance problems come from unnecessary re-renders, not slow JavaScript. A component that renders 10 times when it should render once costs more than optimizing the render itself. The solution is finding where the unnecessary renders come from — not adding useMemo everywhere.
Step 1: Find the Problem with React DevTools Profiler
Before any optimization, measure. Open React DevTools → Profiler tab → Record → interact with the slow part of your app → Stop.
The Profiler shows: - Which components rendered - How long each render took - Why each component rendered (props changed, state changed, parent rendered)
A component marked "Rendered because parent rendered" with no changed props is a candidate for React.memo.
When useMemo Actually Helps
useMemo caches the result of a computation:
// Without useMemo — recalculated on every render
const expensiveResult = computeExpensiveValue(data);
// With useMemo — recalculated only when data changes
const expensiveResult = useMemo(() => computeExpensiveValue(data), [data]);
useMemo is worth using when:
1. The computation is genuinely expensive (sorting/filtering thousands of items)
2. The result is used as a dependency in another useMemo or useEffect
3. The result is passed as a prop to a memoized child component
useMemo is not worth using for:
- Simple arithmetic or string operations
- Object creation that is not used as a prop or dependency
- Everything "just in case" — it adds overhead itself
// Not worth it — the computation is trivial
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// Just do:
const fullName = `${firstName} ${lastName}`;
When useCallback Actually Helps
useCallback caches a function reference:
// Without useCallback — new function reference on every render
const handleClick = () => doSomething(id);
// With useCallback — same reference unless id changes
const handleClick = useCallback(() => doSomething(id), [id]);
useCallback matters only when the function is passed as a prop to a memoized child, or used as a useEffect dependency. A new function reference every render causes the child to re-render (defeating React.memo) or the effect to re-run.
// Useful: handleSubmit is passed to a memoized form component
const handleSubmit = useCallback(async (data) => {
await saveData(data);
navigate("/success");
}, [navigate]);
return <MemoizedForm onSubmit={handleSubmit} />;
React.memo: Prevent Child Re-renders
React.memo prevents a component from re-rendering when its props have not changed:
const UserCard = React.memo(({ user, onSelect }) => {
return (
<div onClick={() => onSelect(user.id)}>
{user.name}
</div>
);
});
For React.memo to work, the props must be stable references. If onSelect is a new function every render, React.memo does nothing — pair it with useCallback:
// In the parent:
const handleSelect = useCallback((id) => selectUser(id), []);
return <UserCard user={user} onSelect={handleSelect} />;
Code Splitting with React.lazy
Large JavaScript bundles block initial page load. Split your bundle by route so users only download what they need:
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const Reports = lazy(() => import("./pages/Reports"));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
Each lazy import becomes a separate chunk. The browser downloads only the current route's code — the Dashboard chunk is not downloaded until the user navigates to /dashboard.
Virtual Lists for Large Datasets
Rendering 1,000 list items is slow — the DOM has 1,000 nodes. Virtual lists render only the visible items:
npm install @tanstack/react-virtual
import { useVirtualizer } from "@tanstack/react-virtual";
function LargeList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60, // Estimated row height in pixels
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.index}
style={{
position: "absolute",
top: `${virtualRow.start}px`,
width: "100%",
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
Instead of 1,000 DOM nodes, only 10–15 visible items are rendered at any time. The performance improvement for lists over ~200 items is dramatic.
State Colocation: The Most Underrated Optimization
Moving state down reduces how many components re-render on state changes:
// BAD: search query in parent causes whole list to re-render
function App() {
const [search, setSearch] = useState("");
return (
<>
<SearchInput value={search} onChange={setSearch} />
<HeavyDashboard /> {/* Re-renders every keystroke */}
<UserList search={search} />
</>
);
}
// GOOD: search state in a component that only needs it
function SearchSection() {
const [search, setSearch] = useState("");
return (
<>
<SearchInput value={search} onChange={setSearch} />
<UserList search={search} />
</>
);
}
function App() {
return (
<>
<SearchSection />
<HeavyDashboard /> {/* No longer re-renders on search */}
</>
);
}
Colocating state is often more effective than useMemo or React.memo — and requires no special APIs.
The Premature Optimization Trap
Wrapping every function in useCallback and every value in useMemo does not make your app faster — it makes it more complex. useMemo and useCallback have a cost: creating and caching the value, running the comparison on every render. For cheap operations, the overhead exceeds the savings.
The correct order:
1. Build the feature correctly first
2. Measure with the Profiler if you notice slowness
3. Fix the actual bottleneck (usually state location or unnecessary re-renders)
4. Apply useMemo/useCallback/React.memo only where the Profiler confirms it helps
If you need a performant React application — properly structured, optimized where it matters, and tested — hire me as a React developer or as a frontend developer.