React Performance Interview Questions and Answers

Unnecessary re-renders, React.memo, code splitting, virtualisation, the Profiler and the React Compiler, with the measurement-first answers interviewers expect.

14 questions1 fresher, 9 mid-level, 4 senior

There is one answer pattern that works for every performance question in a React interview: measure, find the actual cause, fix that. Candidates who open with "I would wrap everything in useMemo" lose the point immediately. Candidates who say "I would record a Profiler session and look at what is committing" have already answered half of the follow-ups.

It also helps to separate the two kinds of slow: slow to load, which is a bundle and network problem, and slow to interact, which is a rendering problem. They have completely different fixes.

1.How would you improve the performance of a React application?

Mid-level

Measure first with the Profiler and a Lighthouse run, then fix what you found: cut the bundle with code splitting, cut unnecessary renders with memoisation and better state placement, and cut the work per render with virtualisation.

Load performance

  • Split the bundle by route with React.lazy and Suspense, and lazily load heavy widgets such as editors and charts.
  • Audit dependencies. One date or icon library imported wholesale is often the largest chunk in an app.
  • Server render or statically generate the first screen so the user sees content before the JavaScript arrives.

Runtime performance

  • Move state down to the component that uses it, so a keystroke does not re-render a page.
  • Memoise components and values that measurably matter: React.memo, useMemo, useCallback.
  • Virtualise long lists so the DOM holds the visible rows only.
  • Mark non-urgent updates with useTransition or useDeferredValue so typing stays responsive.
  • Keep list keys stable so React moves nodes instead of recreating them.

Likely follow-up questions

  • Which of those would you do first?
  • How would you know it worked?

2.What is React.memo and when does it not help?

Mid-level

React.memo wraps a component so React skips re-rendering it when its props are shallowly equal to last time. It does nothing when the props include a new object, array or function each render, or when the component re-renders because of its own state or context.

const Row = React.memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.label}</li>;
});

// Defeated: a new object and a new function on every parent render
<Row item={{ ...item }} onSelect={() => select(item.id)} />

// Works: stable references
<Row item={item} onSelect={handleSelect} /> // handleSelect from useCallback
  • It compares props shallowly. Pass a second argument to supply your own comparison, but a deep compare is usually more expensive than the render.
  • It cannot stop a re-render caused by useState, useReducer or a context the component consumes.
  • It is worth it for components that render often with the same props and are expensive: big lists, charts, heavy tables. For a component that renders a span, it is noise.

Likely follow-up questions

  • Why is my memoised component still re-rendering?
  • What is the difference between React.memo and useMemo?
Practise this in the quiz

3.What causes unnecessary re-renders, and how do you find them?

Mid-level

State that lives too high in the tree, new object or function references passed as props, context values that change identity, and missing memoisation on expensive children. You find them with the React DevTools Profiler and the "highlight updates" option.

How to find them

  1. Turn on Highlight updates when components render in React DevTools to see what flashes when you interact.
  2. Record a Profiler session, then read the flamegraph: wide bars are expensive renders, and the "Why did this render?" panel names the prop or hook responsible.
  3. Check the ranked chart to see which component actually costs the most, rather than which renders most often.

The usual causes

  • A form's input state held in a page level component, so every keystroke re-renders the page.
  • An object or arrow function created inline and passed to a memoised child.
  • A context provider whose value is a fresh object literal every render.
  • A parent re-rendering for an unrelated reason and dragging an expensive subtree with it.

Likely follow-up questions

  • Where would you move the state in that form example?
  • Is a re-render the same as a DOM update?
Practise this in the quiz

4.What is code splitting, and how do React.lazy and Suspense implement it?

Mid-level

Code splitting breaks the bundle into chunks loaded on demand instead of one file up front. React.lazy takes a dynamic import and returns a component; Suspense renders a fallback while that chunk downloads.

const Dashboard = React.lazy(() => import('./Dashboard'));

<Suspense fallback={<Skeleton />}>
  <Dashboard />
</Suspense>
  • Route level splitting is the highest value split: a user on the login page should not download the admin area.
  • Component level splitting pays off for heavy, rarely opened things: a rich text editor, a charting library, a map.
  • Wrap lazy routes in an error boundary as well, because a chunk can fail to load on a flaky network or after a deploy.
  • Splitting too finely costs you: many small requests can be slower than one reasonable chunk.

Likely follow-up questions

  • What happens if the chunk fails to load?
  • How would you preload a chunk before the user needs it?
Practise this in the quiz

5.What is the difference between code splitting and lazy loading?

Fresher

Code splitting is a build time concern: the bundler produces several chunks. Lazy loading is the runtime behaviour: a chunk or an asset is requested only when it is needed. Code splitting makes lazy loading of code possible.

The terms are often used interchangeably, and the distinction is worth making because lazy loading also applies to things the bundler never touches, such as images with loading="lazy" or data fetched when a panel opens.

Likely follow-up questions

  • How do you lazy load an image?
Practise this in the quiz

6.What is list virtualisation and when do you need it?

Mid-level

Virtualisation renders only the rows currently visible plus a small buffer, and reuses them as the user scrolls. You need it once a list is large enough that the number of DOM nodes, not React, is the bottleneck — typically from a few hundred rows.

Ten thousand rows means tens of thousands of DOM nodes, each with layout and style cost, plus the memory to hold them. Virtualisation keeps the DOM at the size of the viewport no matter how long the list is.

TanStack Virtual, react-window and react-virtuoso all do this.
const virtualiser = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 48,
});

Likely follow-up questions

  • What breaks when you virtualise a list?

7.How do you use the React Profiler?

Mid-level

Record an interaction in the React DevTools Profiler tab and read the commits: the flamegraph shows what rendered and how long it took, and each component shows why it re-rendered. There is also a Profiler component for measuring in code.

  • Flamegraph shows the component tree for one commit, with width proportional to render time.
  • Ranked chart sorts components by time spent, which is where you start.
  • Why did this render? attributes a render to a changed prop, a hook, or the parent, and is the single most useful panel.
  • Enable record why each component rendered in the profiler settings before recording, or that panel stays empty.
The in-code Profiler, useful for logging timings in an automated test.
<Profiler id='Sidebar' onRender={(id, phase, actualDuration) => {
  track(id, phase, actualDuration);
}}>
  <Sidebar />
</Profiler>

Likely follow-up questions

  • What is the difference between actualDuration and baseDuration?
Practise this in the quiz

8.Does the React Compiler make useMemo and useCallback unnecessary?

Senior

Largely, for code it can compile. The React Compiler memoises components and values automatically at build time, so most manual useMemo and useCallback calls become redundant — but only if your components follow the rules of React.

The compiler analyses your components and inserts the memoisation you would have written by hand, at a finer granularity than a human would bother with. It bails out of components it cannot prove are safe, which is why it comes with a lint rule that reports the code stopping it.

  • It relies on components and hooks being pure: no mutation of props or state, no side effects during render.
  • Existing useMemo and useCallback calls are not errors, they simply become unnecessary in compiled code.
  • It does not remove the need to think about where state lives — the compiler cannot know that a form's state belongs in the form rather than the page.

Likely follow-up questions

  • What stops the compiler from optimising a component?

9.How do you stop a context change from re-rendering every consumer?

Senior

Split the context by concern and by update frequency, memoise the provider value, and separate state from dispatch so components that only write never re-render. For fine-grained reads, use a store with selectors instead.

Two contexts: the value changes often, dispatch never does.
const StateContext = createContext(null);
const DispatchContext = createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={state}>{children}</StateContext.Provider>
    </DispatchContext.Provider>
  );
}

Context has no selector API: a consumer subscribes to the whole value. If components genuinely need to read one field out of a large, frequently changing object, that is the point at which a store such as Zustand or Redux, which supports selector based subscriptions, is the right tool.

Likely follow-up questions

  • Why does context not have selectors?
Practise this in the quiz

10.Why can passing an inline arrow function as a prop be a problem?

Mid-level

Because it creates a new function identity on every render, which breaks the shallow prop comparison in React.memo and re-triggers any effect that depends on it. On a plain DOM element it costs nothing worth worrying about.

// Fine: button does not care about identity
<button onClick={() => setOpen(true)}>Open</button>

// Not fine: MemoisedList re-renders every time despite React.memo
<MemoisedList onSelect={(id) => select(id)} />

Likely follow-up questions

  • When is an inline function perfectly fine?
Practise this in the quiz

11.How do you debounce or throttle in React, and where does the timer live?

Mid-level

Debounce the value with a custom hook and an effect that clears its timeout on cleanup, or debounce the callback with a ref so the timer survives re-renders. Never create the debounced function inline in the component body — it is recreated every render, so it never fires.

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

const debouncedQuery = useDebouncedValue(query);
useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);

Debounce waits for a pause: use it for search-as-you-type and autosave. Throttle guarantees a maximum rate: use it for scroll and resize handlers. For expensive rendering rather than expensive requests, useDeferredValue is often the better React-native answer.

Likely follow-up questions

  • When would you use useDeferredValue instead?
  • Why does debounce(fn, 300) in the component body not work?

12.How would you fix a large form where every keystroke feels laggy?

Senior

Move each field's state into the field component so a keystroke re-renders one input rather than the whole form, or switch to uncontrolled inputs with a form library. If an expensive preview must update, defer it with useDeferredValue.

  1. Profile it first and confirm the cost is the re-render, not something in an effect firing on every change.
  2. Colocate the state. One useState per field inside the field component, with the parent only reading values on submit.
  3. Or go uncontrolled. React Hook Form keeps values in refs and subscribes only the components that display them, so typing re-renders nothing.
  4. Defer the expensive dependent render. A live preview or validation summary can lag a frame behind with useDeferredValue.
  5. Memoise the heavy children that genuinely cannot be moved.

Likely follow-up questions

  • Why is an uncontrolled form faster?
Practise this in the quiz

13.How do you reduce a React app's bundle size?

Mid-level

Analyse the bundle, split by route, replace or trim oversized dependencies, import only what you use so tree shaking can work, and move rarely used code behind a dynamic import.

  • Run a bundle analyser first. The answer is almost always one or two dependencies, not your own code.
  • Import specific modules: import debounce from 'lodash/debounce' rather than the whole library, and prefer libraries that ship ES modules so tree shaking can drop what you do not use.
  • Check for duplicated dependencies from transitive versions, and for a date or icon library pulling in every locale or every icon.
  • Dynamically import heavy, optional features.
  • Make sure the production build is actually being measured: development React is several times larger.

Likely follow-up questions

  • What is tree shaking and what stops it working?

14.What are Core Web Vitals, and how does a React app affect them?

Senior

LCP measures how quickly the main content appears, INP how quickly the page responds to interaction, and CLS how much the layout jumps. A client rendered React app typically hurts LCP, heavy synchronous renders hurt INP, and unreserved space for images and ads hurts CLS.

MetricWhat it measuresThe React lever
LCPTime until the largest content element paintsServer render or statically generate, cut the critical bundle, preload the hero image
INPResponsiveness to user input across the visitBreak up long renders, use transitions, virtualise lists, avoid blocking layout effects
CLSUnexpected layout movementReserve height for images, ads and async content; avoid inserting banners above content

INP replaced First Input Delay in 2024 and is the one React apps most often fail, because it measures every interaction, not just the first. A single expensive state update on a keystroke is enough to fail it.

Likely follow-up questions

  • Which one is hardest to fix in a single page app?

All React interview questions