ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

React Performance Interview Questions

Short answer

React performance work is diagnosis first: profile to find what actually re-renders, then fix the cause. The usual causes are unstable references defeating memoisation, state placed too high in the tree, unvirtualised long lists, and oversized bundles — in roughly that order of frequency.

SymptomUsual causeThe fix
Typing in an input lagsState too high; whole tree re-rendersMove state down, or split the component
Everything re-renders on any changeContext value not memoiseduseMemo the value, split the context
React.memo appears to do nothingA prop is a new reference each renderuseCallback / useMemo / hoist constants
Long list scrolls badlyThousands of DOM nodesVirtualise
Slow first loadBundle too largeCode split by route, lazy load
Slow after data arrivesExpensive derivation on every renderuseMemo, or compute it once upstream
Janky animationLayout thrash or main-thread workAnimate transform/opacity; move work off render
Symptom to cause — the mapping an interviewer wants you to reason through.

How do you find out what is actually slow?

Measure before you change anything — and say so before you propose a fix, because the most common failure in this round is a candidate reaching for `useMemo` before knowing what is slow. React DevTools has a Profiler that records a commit and shows which components rendered, why, and how long each took. Turning on "Highlight updates when components render" is even faster for a first look: interact with the page and watch what flashes that had no reason to.

The distinction that makes your answer credible is between a render problem and a load problem. If the app is slow to appear, the bundle and the network are the suspects and the Network panel plus a Lighthouse run will tell you. If it is slow while you use it, the Profiler is the tool. Naming which one you are diagnosing, and therefore which tool you would open, is a stronger opening than any specific optimisation.

Why does React.memo often do nothing?

Because it compares props with `Object.is` — a shallow, reference-based check — and JSX creates fresh objects, arrays and functions on every render. An inline arrow function, an object literal, an array literal or children passed as JSX are all new references each time, so the comparison always reports a change and the memo never bails out. Wrapping a component in `React.memo` while the parent passes any of those is a no-op that costs you an extra comparison.

Four ways to defeat memo, and the fixesjsx
1const Row = React.memo(RowImpl);
2
3<Row user={u} onSelect={() => pick(u.id)} />   // new function
4<Row user={u} tags={[]} />                     // new array
5<Row user={{ ...u }} />                        // new object
6<Row user={u}><Badge /></Row>                  // children: new element
7// → memo never bails out in any of these
8
9// Fixes, in order of preference:
10const NO_TAGS = [];                            // module scope — free
11const handleSelect = useCallback((id) => pick(id), [pick]);
12const style = useMemo(() => ({ w, h }), [w, h]);
13
14<Row user={u} tags={NO_TAGS} onSelect={handleSelect} />
15// → now Row re-renders only when `user` actually changes

There is a second reason memo appears not to work, and it is worth knowing because it is the follow-up. `React.memo` only blocks re-renders caused by a parent rendering. If the component has its own state, or consumes a context whose value changed, it re-renders regardless of how its props compare — the update reaches it through a different path entirely.

It is worth knowing why memoisation has to be unbroken to work. A `useCallback` whose dependency is itself an unstable function from the parent produces a new reference anyway, so the chain fails one level up and every layer below it is wasted work. That maintenance burden — every link having to hold — is the strongest practical argument for using memoisation deliberately and sparingly rather than as a default habit.

When should you use useMemo and useCallback?

For two distinct reasons, and being able to name both is the answer. The first is skipping genuinely expensive work — a sort or filter over thousands of items that would otherwise run on every keystroke elsewhere in the component. The second, and more common in practice, is keeping a reference stable so something downstream can rely on it: a memoised child, another hook's dependency array, or a context value.

Both reasons, and one case that is not worth itjsx
1// Reason 1 — the work is expensive
2const sorted = useMemo(() => rows.slice().sort(byPrice), [rows]);
3// → a 5,000-row sort runs once per change of `rows`
4
5// Reason 2 — the REFERENCE is the point
6const options = useMemo(() => ({ theme: "dark" }), []);
7<Chart options={options} />
8// → same reference every render, so React.memo on Chart works
9
10// Not worth memoising — the memo costs more than the work
11const total = useMemo(() => a + b, [a, b]);   // ← just write a + b
12
13// useCallback is useMemo for functions:
14useCallback(fn, deps) === useMemo(() => fn, deps)

The cost side matters too, and mentioning it unprompted reads well. Every memo allocates a dependency array, compares it on each render, and holds its result for the lifetime of the component. For a sum or a string concatenation that bookkeeping costs more than the work it avoids, and it makes the code harder to read. React also documents `useMemo` as a hint — it may discard cached values — so it must never be relied on for correctness.

A useful sanity check before adding any memo: ask what the child actually costs to render. Memoising a component that renders a handful of nodes usually loses, because comparing its props costs more than simply rendering it. Memoisation pays off at boundaries — the root of a large subtree, a list row rendered hundreds of times, a chart — and that is where an interviewer expects to see it applied.

How does state placement affect performance?

More than any hook. When state changes, React re-renders that component and everything below it, so state sitting near the root means the whole tree re-renders on every keystroke. Moving it down to the smallest component that needs it is usually a bigger win than any amount of memoisation, and it is a structural fix rather than a patch.

Two structural fixes that beat memoisingjsx
1// SLOW — every keystroke re-renders <HeavyList />
2function Page() {
3  const [q, setQ] = useState("");
4  return (
5    <>
6      <input value={q} onChange={e => setQ(e.target.value)} />
7      <HeavyList />
8    </>
9  );
10}
11
12// FIX 1 — push the state into a smaller component
13function SearchBox() {
14  const [q, setQ] = useState("");
15  return <input value={q} onChange={e => setQ(e.target.value)} />;
16}
17// → <HeavyList /> is now a sibling and never re-renders
18
19// FIX 2 — children as a prop: the heavy tree is created by the
20// PARENT, so it is the same element object across re-renders
21function Wrapper({ children }) {
22  const [q, setQ] = useState("");
23  return <><input value={q} onChange={e => setQ(e.target.value)} />{children}</>;
24}
25// <Wrapper><HeavyList /></Wrapper>

That second pattern surprises people and is a genuinely strong thing to know. Because `children` is created by the parent and passed in, re-rendering `Wrapper` does not recreate that element — React sees the identical object and skips the subtree. It is memoisation you get from composition rather than from a hook, and it is why "lift content out, pass it as children" appears so often in React performance advice.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

How do you handle very long lists?

Virtualise them. Rendering ten thousand rows means ten thousand DOM nodes, which costs memory, makes every layout calculation slower and makes scrolling janky regardless of how well memoised the rows are. Virtualisation renders only the rows in the viewport plus a small overscan buffer, so the DOM node count stays constant no matter how long the list gets.

Constant DOM cost, whatever the list lengthjsx
1const virtualizer = useVirtualizer({
2  count: rows.length,          // 10,000
3  getScrollElement: () => parentRef.current,
4  estimateSize: () => 48,
5  overscan: 5,
6});
7
8return (
9  <div ref={parentRef} style={{ overflow: "auto", height: 600 }}>
10    <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
11      {virtualizer.getVirtualItems().map(v => (
12        <Row key={rows[v.index].id} row={rows[v.index]}
13             style={{ transform: `translateY(${v.start}px)` }} />
14      ))}
15    </div>
16  </div>
17);
18// → ~18 DOM nodes on screen instead of 10,000, and the spacer
19//   div keeps the scrollbar the right size

How do you reduce bundle size and improve load time?

Split by route first — it is the highest-return change and usually the easiest, because a user landing on the login page has no reason to download the dashboard's charting library. Then split heavy components that are not needed immediately: modals, editors, date pickers, anything below the fold. `React.lazy` with `Suspense` covers both, and any modern bundler handles the mechanics.

Route splitting and deferring a heavy componentjsx
1const Dashboard = lazy(() => import("./Dashboard"));
2
3<Suspense fallback={<Spinner />}>
4  <Route path="/dashboard" element={<Dashboard />} />
5</Suspense>
6// → Dashboard's chunk downloads only when that route is visited
7
8// Defer a heavy widget until it is actually opened
9const Editor = lazy(() => import("./RichTextEditor"));
10{isOpen && <Suspense fallback={null}><Editor /></Suspense>}
11
12// Preload on intent, so the wait is hidden:
13<button onMouseEnter={() => import("./RichTextEditor")}>Edit</button>
14// → the chunk is already in flight before the click lands

Then look at what is in the bundle rather than guessing. A bundle analyser regularly turns up a date library imported whole for one function, an icon set pulled in entirely for six icons, or two versions of the same dependency. Those are usually larger wins than any code you would write, and being able to say "I'd run the analyser before splitting anything" keeps the diagnosis-first framing consistent.

Bundle work has a second dimension people forget to mention: what the JavaScript costs to execute, not just to download. On a mid-range Android phone, parsing and running a large bundle can take longer than fetching it, which is why shipping less code beats compressing the same code. For an India-first product that is the constraint that actually decides how the app feels, and naming it shows you design for real devices rather than for a development laptop.

What are useTransition and useDeferredValue for?

Both let you mark work as low priority so the interface stays responsive while it happens. `useTransition` wraps a state update you are willing to have interrupted and gives you a pending flag. `useDeferredValue` takes a value and returns a version that lags behind, so an expensive component can render with the old value while the new one is prepared. Neither makes the work faster — they change what the user waits for.

Keeping the input responsive while a heavy list catches upjsx
1function Search() {
2  const [q, setQ] = useState("");
3  const deferred = useDeferredValue(q);
4
5  return (
6    <>
7      <input value={q} onChange={e => setQ(e.target.value)} />
8      <HeavyResults query={deferred} />
9    </>
10  );
11  // → the input updates on every keystroke; HeavyResults renders
12  //   with a slightly stale query instead of blocking typing
13}
14
15const [isPending, startTransition] = useTransition();
16startTransition(() => setTab(next));
17// → the tab switch can be interrupted; isPending drives a spinner

The distinction from debouncing is a good follow-up to have ready. Debouncing delays when work starts, on a timer you pick; a deferred value starts the work immediately but at a priority React can interrupt. For a network request, debounce is what you want, because the goal is fewer requests. For an expensive render over data you already have, `useDeferredValue` is better, because there is no arbitrary delay to tune.

What does React Compiler change, and what does it not fix?

React Compiler inserts memoisation at build time, which removes most hand-written `useMemo`, `useCallback` and `React.memo`. In a project where it is enabled and not bailing out, the reference-identity problems described earlier largely disappear — which is genuinely a large share of everyday React performance work.

What it does not touch is everything structural. It will not virtualise a list, split your bundle, move state down the tree, remove a request waterfall, or stop a layout thrashing during scroll. It also cannot help components it bails out on, which is why not mutating props or state matters more once it is on. Saying that clearly is a strong close to any React performance answer, because it shows you know where the tool's boundary is.

Should I memoise everything just in case?

No. Every memo costs an allocation, a comparison on each render and some retained memory, and it makes the code harder to read. For cheap computations that is a net loss. Memoise where a profile shows it matters, or where a stable reference is required by a memoised child or a hook dependency.

Does a re-render mean the DOM was updated?

No, and conflating the two is a common mistake. A re-render means the component function ran and React compared the result with the previous one. If nothing differs, no DOM changes. Renders are usually cheap; the cost comes from very frequent renders, expensive work inside them, or huge subtrees.

Why does my component render twice in development?

Strict Mode deliberately double-invokes components and effects in development to surface side effects and missing cleanup. It does not happen in production builds, so it is not a performance problem — but it does mean you should profile a production build before drawing conclusions about timing.

How do you optimise images in a React app?

Serve modern formats, size them for the actual display width, lazy-load anything below the fold, and always set width and height so the layout does not shift. In Next.js the Image component does most of this. Images are frequently the largest thing on the page, so this often beats any JavaScript optimisation.

What is the difference between useMemo and useCallback?

useMemo returns the result of calling the function you pass; useCallback returns the function itself, uncalled. useCallback(fn, deps) is defined as useMemo(() => fn, deps). Use the first for values, the second for handlers you pass to memoised children.

Frequently asked questions

How do you improve React performance?
Profile first to establish whether it is a load problem or a render problem. Then fix the cause: move state down, stabilise references so memoisation works, virtualise long lists, split the bundle by route, and defer expensive renders with useDeferredValue. Reaching for useMemo before diagnosing is the most common mistake.
Why is React.memo not working?
Almost always because a prop is a new reference on every render — an inline arrow function, an object or array literal, or JSX children. Shallow comparison sees a change and re-renders. The other reason is that memo only blocks parent-driven re-renders; own state and context updates bypass it entirely.
When should you use useMemo?
When a computation is expensive enough to show up in a profile, or when a value's reference must stay stable for a memoised child, a hook dependency array, or a context value. For simple arithmetic and string building, the memo costs more than the work.
What is the difference between useMemo and useCallback?
useMemo returns the value produced by calling the function you give it; useCallback returns that function itself without calling it. They are the same mechanism — useCallback(fn, deps) is exactly useMemo(() => fn, deps) — with different return values.
Does re-rendering mean the DOM updates?
No. A re-render runs the component function and produces a new element tree, which React compares with the previous one. Only actual differences reach the DOM. Renders are usually inexpensive; problems come from very frequent renders, costly work inside them, or very large subtrees.
How do you optimise a long list in React?
Virtualise it so only the visible rows plus a small overscan exist in the DOM, keeping node count constant regardless of list length. Combine with stable id keys, memoised row components, and pagination on the data side so the payload stays bounded.
What is code splitting in React?
Breaking the bundle into chunks loaded on demand, usually with React.lazy and Suspense. Split by route first, then by heavy components that are not needed immediately — modals, editors, charts. Preloading on hover hides most of the remaining wait.
What is the difference between useDeferredValue and debouncing?
Debouncing delays when work starts, using a timer you choose. useDeferredValue starts it immediately but at an interruptible priority, so the interface stays responsive. Debounce network requests; defer expensive renders over data you already have.
How does state placement affect performance?
A state change re-renders that component and its entire subtree, so state near the root means the whole tree re-renders on every update. Moving state to the smallest component that needs it is usually a larger and more durable win than adding memoisation.
Why is passing children as a prop a performance technique?
Because the children element is created by the parent and passed in, so re-rendering the wrapper does not recreate it. React sees an identical element object and skips that subtree. It is memoisation obtained through composition instead of a hook.
Does React Compiler mean I no longer need to think about performance?
No. It removes most manual memoisation, which is a real share of everyday work, but it cannot virtualise a list, split a bundle, move state, or fix a request waterfall. Those are structural problems and remain what performance interviews focus on.
How do you profile a React application?
Use the Profiler tab in React DevTools to record an interaction and see which components rendered, why, and for how long. "Highlight updates when components render" gives a faster first read. Profile a production build, since Strict Mode double-invokes components in development.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399
Written by Arun Karthikeyan · Last updated

Full kit

React Interview Kit · ₹399

Get it →