ReactPerformancebeginner
Updated:

React Performance Optimization

6 min read

A hands-on guide to react performance optimization: find wasted re-renders, fix them with memo, useMemo and useCallback, and profile before you optimize.

TL;DR – Quick Answer

React performance optimization is the practice of reducing unnecessary work the browser does when your UI updates, mostly by preventing components from re-rendering when their data has not changed. You do it with React.memo, useMemo and useCallback, stable keys on lists, and code splitting with lazy and Suspense. Always profile with the React DevTools Profiler first so you fix the render that actually hurts, not a guess.

On This Page

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 useMemo and useCallback "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:

  1. Profile first. Record with the Profiler and find the actual slow or over-rendering component. Do not skip this.
  2. Move state down. Put fast-changing state in the smallest component that needs it.
  3. Fix keys. Replace index keys with stable ids on every list.
  4. Memoize the hotspot. Wrap the proven-expensive child in React.memo, then stabilize its object and function props with useMemo and useCallback.
  5. Virtualize big lists. Anything over a few hundred rows.
  6. 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 with useMemo/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?
Use React.memo on a component that renders often with the same props, especially a pure presentational component inside a list or a frequently updating parent. It compares props shallowly and skips the render if nothing changed. Do not wrap every component in it, because the comparison itself has a small cost and most cheap components do not benefit.
What is the difference between useMemo and useCallback?
useMemo caches the result of a calculation and returns the same value until its dependencies change. useCallback caches a function definition so it keeps the same reference across renders. useCallback(fn, deps) is really just useMemo(() => fn, deps), so they solve the same reference-stability problem for values versus functions.
Why does my whole component tree re-render on every keystroke?
Because state lives too high in the tree. When a parent re-renders, all its children re-render by default, even the ones that did not change. Move the fast-changing state down into the smallest component that needs it, or split the input into its own component so typing does not touch the rest of the page.
Does using index as a key hurt performance?
It can, and it also causes bugs. When list items reorder, insert or delete, index keys make React reuse the wrong DOM nodes, leading to lost input state and extra work. Use a stable unique id from your data as the key so React can match items correctly across renders.
Should I optimize React performance from the start?
No. Write clear, correct code first, then measure with the Profiler when something feels slow. Premature memoization adds complexity and can even make code slower. Optimize the specific slow render you can measure, not the one you imagine.
What is code splitting in React?
Code splitting breaks your JavaScript bundle into smaller chunks that load on demand instead of all at once. You do it with React.lazy and Suspense to load a route or heavy component only when the user needs it. This shrinks the initial download and speeds up first paint.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us