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?
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
Turn on Highlight updates when components render in React DevTools to see what flashes when you interact.
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.
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?
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.
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.
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.
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.
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.
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.
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)} />
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.
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.
Profile it first and confirm the cost is the re-render, not something in an effect firing on every change.
Colocate the state. One useState per field inside the field component, with the parent only reading values on submit.
Or go uncontrolled. React Hook Form keeps values in refs and subscribes only the components that display them, so typing re-renders nothing.
Defer the expensive dependent render. A live preview or validation summary can lag a frame behind with useDeferredValue.
Memoise the heavy children that genuinely cannot be moved.
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.
Metric
What it measures
The React lever
LCP
Time until the largest content element paints
Server render or statically generate, cut the critical bundle, preload the hero image
INP
Responsiveness to user input across the visit
Break up long renders, use transitions, virtualise lists, avoid blocking layout effects
CLS
Unexpected layout movement
Reserve 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.