Most React apps are not slow because React is slow. They are slow because components re-render far more often than they need to, and each wasted render runs your JSX, diffs a virtual DOM tree, and sometimes touches the real DOM for nothing. Performance work in React is mostly the art of stopping that wasted work.
This guide walks the practical path a working developer takes: understand when React
re-renders, find the expensive renders with the Profiler, then apply memo, useMemo,
useCallback, stable keys, and code splitting only where they earn their keep.
When does React actually re-render?
A component re-renders in exactly three situations: its own state changes, a context value it consumes changes, or its parent re-renders. The third one surprises everyone. By default, when a parent renders, every child renders too, whether or not its props changed.
That default is usually fine. Re-rendering is not the same as touching the DOM — React compares the new virtual tree with the old one and only writes the real DOM where something differs. The problem is when the render function itself is expensive, or when thousands of components re-render on every keystroke.
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList /> {/* re-renders on every click, for nothing */}
</div>
);
}
Here ExpensiveList has no props tied to count, yet it re-renders every time the
button is clicked, because App re-renders. That is the pattern you learn to spot.
Pro tip: Re-rendering is cheap until it isn't. Do not chase every render. Chase the renders the React DevTools Profiler highlights in yellow and red — those are the ones your users feel.
Step one: measure with the Profiler
Never optimize by guessing. Install React DevTools, open the Profiler tab, click record, interact with your app, and stop. The flamegraph shows every component that rendered, how long it took, and why it rendered. Wide yellow bars are your targets.
The Profiler answers the two questions that matter: which component is slow, and is it rendering when it shouldn't. Fixing the wrong component is the most common waste of a developer's afternoon. If you are still building your first app, set up a project the fast way with the Vite setup guide so hot reload and the Profiler work smoothly.
Prevent wasted renders with React.memo
React.memo wraps a component and skips its render when its props have not changed
(compared shallowly). Apply it to the pure child that keeps re-rendering for no reason:
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log("rendering list"); // now only logs when items changes
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
Now clicking the counter no longer re-renders ExpensiveList, because its items prop
is unchanged. memo does a shallow comparison: primitives compare by value, but objects,
arrays and functions compare by reference. That reference detail is where the next two
hooks come in.
Stabilize values with useMemo and useCallback
memo only helps if the props you pass stay referentially stable. If a parent creates a
new array or a new function on every render, the child sees a "new" prop and re-renders
anyway.
function Dashboard({ rawItems, query }) {
// useMemo: don't re-filter 10,000 rows unless inputs change
const visibleItems = useMemo(
() => rawItems.filter((i) => i.name.includes(query)),
[rawItems, query]
);
// useCallback: keep the same function reference across renders
const handleSelect = useCallback((id) => {
console.log("selected", id);
}, []);
return <ExpensiveList items={visibleItems} onSelect={handleSelect} />;
}
useMemo caches an expensive calculation — the filtered list here — and only recomputes
when rawItems or query change. useCallback caches a function so its reference
survives across renders, which lets the memoized child skip its render. In fact
useCallback(fn, deps) is just shorthand for useMemo(() => fn, deps).
If the hook rules feel shaky, the hooks introduction and the deeper useState and useEffect walkthrough cover the dependency array in detail — getting dependencies right is the whole game here.
Common mistake: Wrapping everything in
useMemoanduseCallback"to be safe". Each one stores a value and runs a comparison, so on cheap work it costs more than it saves. Reach for them only when a memoized child needs a stable prop, or when a calculation is genuinely heavy.
Lists: keys and virtualization
Long lists are where beginners meet their first real performance wall. Two rules keep lists fast.
First, give every item a stable, unique key from your data, never the array index. Index keys make React reuse the wrong DOM nodes when items are inserted, removed or reordered — that causes both wrong output and extra work.
{users.map((user) => (
<UserRow key={user.id} user={user} /> // stable id, not the index
))}
Second, when a list has thousands of rows, do not render them all. Virtualization
renders only the rows currently visible in the viewport, recycling DOM nodes as the user
scrolls. Libraries like react-window handle this. A 10,000-row table that rendered 10,000
DOM nodes drops to rendering the ~20 that are on screen, and scroll performance transforms.
Split state so typing stays fast
The single highest-impact fix in most apps is not a hook — it is moving state to the right place. If a text input's state sits at the top of a large page, every keystroke re-renders the whole page.
// Slow: search state high up re-renders the entire dashboard on each keystroke
function Page() {
const [q, setQ] = useState("");
return (
<>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<HugeReportTable /> {/* re-renders on every keystroke */}
</>
);
}
Pull the input and its state into their own small SearchBox component, and lift the
results up only when the user submits. Now typing only re-renders SearchBox, and the
heavy table sits still. This is the same lesson taught in the
state management overview:
keep fast-changing state as low and as local as possible.
Code splitting with lazy and Suspense
Everything above trims runtime renders. Code splitting trims the initial download. Instead of shipping one giant bundle, you load heavy routes or components on demand:
import { lazy, Suspense } from "react";
const AnalyticsPanel = lazy(() => import("./AnalyticsPanel"));
function App() {
return (
<Suspense fallback={<p>Loading analytics…</p>}>
<AnalyticsPanel />
</Suspense>
);
}
lazy tells the bundler to split AnalyticsPanel into its own chunk that downloads only
when it first renders. Suspense shows a fallback while that chunk loads. Route-level
splitting — one chunk per page — is the highest-leverage version, because a user visiting
the login page never downloads the admin dashboard's code. Frameworks like Next.js do a
lot of this automatically, which the
Next.js introduction explains.
A practical optimization checklist
When a screen feels slow, work in this order:
- Profile first. Record with the Profiler and find the actual slow or over-rendering component. Do not skip this.
- Move state down. Put fast-changing state in the smallest component that needs it.
- Fix keys. Replace index keys with stable ids on every list.
- Memoize the hotspot. Wrap the proven-expensive child in
React.memo, then stabilize its object and function props withuseMemoanduseCallback. - Virtualize big lists. Anything over a few hundred rows.
- Code split. Lazy-load heavy routes and components to shrink the first load.
Do them top to bottom and stop when the Profiler shows the render is fast enough. Most apps only need the first three.
Interview note: A favourite question is "how do you prevent a child from re-rendering?" The complete answer names all three levers: wrap the child in
React.memo, and keep its object/function props stable withuseMemo/useCallback. Mentioning that you would Profile first shows senior judgement.
Where this fits in your React journey
Performance is a topic interviewers use to separate people who have shipped real apps from people who have only followed tutorials. If you can explain why a component re-renders and when memoization helps, you are ahead of most freshers. Pair this with the React interview hooks questions to rehearse the phrasing.
At CodeBegun's Java Full Stack with AI program in Hyderabad, students build a real dashboard, profile it, and fix the exact over-rendering patterns above — so the concepts land as muscle memory, not just theory. Learn to measure first, optimize second, and you will write React that stays fast as it grows.
Frequently Asked Questions
When should I use React.memo?
What is the difference between useMemo and useCallback?
Why does my whole component tree re-render on every keystroke?
Does using index as a key hurt performance?
Should I optimize React performance from the start?
What is code splitting in React?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

