skip to content

Performance and Memoization

This is where React-specific optimization lives: what actually causes a re-render, how to prove it with a profiler, and which memoization, splitting, or virtualization technique is worth its cost. Interviewers use it to separate people who memoize by reflex from people who measure first.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In React 19, how does React.lazy let a route component ship in its own chunk — what must the imported module expose, and what must be present in the tree when the lazy component first renders?

level: juniorimportance: must knowfreq 70%

answer

  1. two APIs, one boundary
  2. the import call must be dynamic
  3. the module shape React expects
  4. default export, or reshape it
  5. nearest ancestor supplies the placeholder

basics

~20 s

React.lazy(() => import('./Page')) returns a component whose code loads on first render. The imported module must expose that component as its default export, and the lazy element must render inside a Suspense boundary that supplies a fallback.

solid answer

~40 s

`React.lazy` takes a factory that returns a promise — normally `() => import('./Settings')` — and gives you back a component. Because the `import()` is inside a function, the bundler puts that module in a separate chunk that is only fetched the first time the component actually renders. Two contracts come with it. First, the resolved module must have a `default` export that is a React component; for a named export you adapt it with `import('./Chart').then(m => ({ default: m.Chart }))`. Second, the lazy element must have a `<Suspense>` ancestor, because while the chunk is in flight React suspends that subtree and renders the nearest boundary's `fallback` instead. Without a boundary React throws. The call to `lazy` itself belongs at module scope, not inside a component body.

go deeper

for a junior

Be able to write the three lines from memory: the lazy constant at module scope, the dynamic import inside it, and a Suspense wrapper with a fallback. Say plainly that the module needs a default export.

for a middle

Explain why the dynamic import() call form is what creates a separate chunk, how React calls the loader exactly once and caches the result, and how to adapt a named export into the { default: Component } shape React requires.

for a senior

Show judgment about boundary placement — one fallback per route versus a tight boundary around a heavy widget — and about which modules are worth splitting at all, given that each split adds a network round trip at first render.

for a principal

Own the strategy: which split points a codebase standardises on, how loading placeholders stay consistent across teams, and how split boundaries interact with the app's server-rendering and preloading story rather than being chosen ad hoc per component.

## The problem it solves A React app compiled into one bundle makes every user download every screen, including the admin panel they will never open and the rich text editor they use once a month. Code splitting breaks the bundle into chunks that are fetched on demand. React's built-in way to express a split point is `React.lazy` paired with `<Suspense>`: `lazy` marks *what* is split, `Suspense` says *what the user sees* while the split code is still travelling over the network. ## The API `lazy` takes one argument — a function (often called the loader or factory) that returns a promise — and returns a component you render like any other: ```jsx import { lazy, Suspense } from 'react'; const Settings = lazy(() => import('./routes/Settings')); function App() { return ( <Suspense fallback={<PageSkeleton />}> <Settings /> </Suspense> ); } ``` The crucial detail is that `import('./routes/Settings')` sits *inside* a function body. A static `import ... from` at the top of the file is resolved at build time and pulled into the current chunk; a call expression is a runtime decision, and every bundler treats it as a chunk boundary. React never fetches anything itself — it just calls your loader at the right moment and waits on the promise you hand back. React calls that loader **once**. It stores the settled result on the lazy component, so a second render of `<Settings />` uses the already-loaded module with no further work. ## The default-export contract The promise must resolve to an object with a `default` property holding a component. That is exactly the shape an ES module namespace object has when the file does `export default function Settings() {…}`, which is why the plain form works with no ceremony. If your component is a named export, reshape the result yourself: ```js const Chart = lazy(() => import('./Chart').then((m) => ({ default: m.Chart })) ); ``` The same trick is how people load one component out of a barrel file, though pointing the loader straight at the real module is better — a barrel drags its siblings into the chunk. ## Why Suspense is mandatory While the loader's promise is pending, the lazy component has nothing to render, so React *suspends*: it walks up from that position to the nearest `<Suspense>` ancestor and renders that boundary's `fallback` in place of its children. When the promise resolves, React re-renders and swaps the real content in. If there is no `<Suspense>` anywhere above, React has nowhere to put a placeholder and the render fails with an error. The boundary does not have to be the immediate parent — any ancestor works, and the *nearest* one wins. One boundary can cover several lazy components, in which case the fallback stays until all of them have loaded. That is a placement decision: a boundary wrapping the whole page turns the entire screen into a skeleton, while a boundary wrapping only a lazy chart keeps the rest of the page interactive. ## Where the `lazy` call goes Declare it at module scope. Calling `lazy` inside a component body produces a brand-new component type on every render, which makes React unmount and remount the subtree instead of reusing it. The idiomatic file has the `lazy` constants next to the imports. ## What it is not - It is not a data-loading tool. `lazy` splits *code*; fetching the data the component needs is a separate concern. - It does not work for anything but components. You cannot `lazy` a hook or a utility function — for those, call `import()` yourself where you need it. - It does not change *when the component renders*, only when its code arrives. The first render of a lazy route still blocks on a network round trip, which is why hover- or focus-triggered preloading is a common companion technique. ## React 19 notes `lazy` is unchanged in React 19 and remains the API for component-level splitting; `use()` reads a promise's *value* inside a render but is not a replacement for a split boundary. Suspense also drives streaming server rendering, so on a server-rendered app the same boundary that shows your fallback in the browser is the unit the server can stream around — but that is the rendering model's concern, not the split point's. ## What interviewers listen for They want to hear the three moving parts named without prompting: the loader returns a promise, the module resolves with a `default` component, and a `Suspense` ancestor supplies the fallback. Candidates who can also say *why* the dynamic call form is what creates the chunk — rather than treating `lazy` as magic that talks to the bundler — are visibly a tier above.

  • Your component is exported as `export function Chart()` with no default export — can you still use React.lazy on it?
    Yes, but you have to hand React the shape it wants: `lazy(() => import('./Chart').then(m => ({ default: m.Chart })))`. React only ever looks at the `default` property of the resolved object, so any promise that settles with `{ default: SomeComponent }` works. Point the import at the real module rather than a barrel file, otherwise the barrel's other exports end up in the chunk.
  • Does the Suspense boundary have to be the direct parent of the lazy component?
    No. React walks up from the suspended component and uses the nearest `<Suspense>` ancestor, wherever it is. One boundary can cover several lazy components, and then its fallback stays visible until all of them have resolved. Placement is a UX decision: a boundary high in the tree replaces a whole screen, a boundary right around the lazy part keeps the surrounding UI on screen and interactive.
  • What happens if you render a lazy component with no Suspense boundary anywhere above it?
    React has nowhere to render a placeholder, so the render fails and the error propagates as an unhandled error — you get a crash rather than a silent blank area. This is why the boundary is described as a requirement of `lazy` rather than an optional nicety. In practice teams put a boundary at the route level so every lazily loaded screen inherits one.

saying these in an interview costs you the question

  • Thinking React.lazy fetches the chunk itself instead of calling your loader
  • Saying a static import at the top can be lazily loaded
  • Assuming named exports work without reshaping to default
  • Believing Suspense is optional and React just renders nothing
  • Calling React.lazy inside the component that renders it

context

open as a page

In React, a component is wrapped in React.memo. Which re-renders does that actually skip, and which does it not prevent?

level: juniorimportance: must knowfreq 62%

basics

~20 s

React.memo skips a re-render caused by the parent, and only when every new prop is the same as the old one by a shallow comparison. Its own state updates, a context change it reads, or any changed prop still re-render it.

open as a page

Both React.memo and useMemo are described as memoization in React, and candidates often use the names interchangeably. What does each one actually cache, and when would you reach for one rather than the other?

level: middleimportance: must knowfreq 72%

basics

~20 s

React.memo caches a component's rendered output and skips re-running it when its props are unchanged. useMemo caches one value inside a component that still re-renders. One avoids a render; the other avoids a computation during a render.

open as a page

What does the React Compiler (babel-plugin-react-compiler) do to your components at build time, and which hand-written memoization does it make redundant?

level: middleimportance: must knowfreq 60%

basics

~20 s

The React Compiler is a build-time Babel plugin that rewrites components to cache their derived values and their JSX in per-instance slots, reusing them when inputs are unchanged. It replaces most hand-written useMemo, useCallback and React.memo.

open as a page

In a React component, `const query = { term, page };` is built in the component body and used as `useEffect(() => { fetchResults(query).then(setRows); }, [query]);`. The effect fires after every render and never settles. Explain the mechanism and give two fixes.

level: middleimportance: must knowfreq 68%

basics

~20 s

Rebuilding query in the component body creates a new object on every render, and React compares each dependency with Object.is, so the effect re-runs, sets state, renders again, and loops. Depend on the primitive fields instead.

open as a page

A parent renders `<Row style={{ padding: 8 }} tags={[]} onSelect={() => onPick(row.id)} />` and `Row` is wrapped in `React.memo`, yet Row re-renders every time the parent renders. Why does React.memo not skip it, and how do you make it skip?

level: middleimportance: must knowfreq 75%

basics

~20 s

React.memo re-renders whenever a prop differs by Object.is. The inline object, array and arrow are created fresh during each parent render, so all three are new references every time and the shallow comparison always reports a change.

open as a page

In a React page component, a search input's value is held in state at the top of the page, and every keystroke re-renders the whole page including a large results table. Describe the structural fix that removes the per-keystroke cost without React.memo or useMemo.

level: middleimportance: must knowfreq 65%

basics

~20 s

Move the state down: extract the input and its useState into a small child component, so a keystroke re-renders only that child instead of the whole page. Lift only the committed or debounced value back up if the table truly needs it.

open as a page

In React, what actually causes a component to re-render? Many candidates answer "its props changed" — explain why that answer is incomplete.

level: middleimportance: must knowfreq 78%

basics

~20 s

A React component re-renders when its own state changes, when its parent re-renders, or when a context it reads publishes a new value. Changed props are a consequence of the parent rendering, not a separate trigger.

open as a page

A React table renders 10,000 rows and scrolling stutters. Explain what list virtualization (windowing) changes about what is in the DOM, and how the scrollbar can still represent the full list.

level: middleimportance: must knowfreq 62%

basics

~20 s

Windowing renders only the rows that intersect the scroll viewport plus a small overscan, so dozens of row elements exist instead of ten thousand. An inner spacer element sized to the full content height keeps the scrollbar and every scroll offset accurate.

open as a page

Typing into a filter input on a React screen feels sluggish. Walk through how you would use the React DevTools Profiler to locate the cause rather than guessing at it.

level: seniorimportance: must knowfreq 58%

basics

~20 s

Record the typing with the Profiler, with "record why each component rendered" enabled. Count the commits per keystroke, open the worst one, use Ranked to check for a hot component and Flamegraph for a wide cascade, read the reason on the highest re-rendering component, then re-record after the fix.

open as a page

A React page holds a search box's value in state on the top-level page component and passes it down to a filtered table and a chart. Typing in the box is visibly laggy. What is the likely cause, and how would you confirm it before changing any code?

level: seniorimportance: must knowfreq 58%

basics

~20 s

The state lives too high, so every keystroke re-renders the whole page subtree including the chart and table. Confirm before fixing: profile a single keystroke and check whether the cost is many cheap renders or one expensive component's render body.

open as a page

React DevTools has a General setting called "Highlight updates when components render". What does it do on the page, and what would you use it for?

level: juniorimportance: should knowfreq 42%

basics

~20 s

It flashes a coloured outline around each region of the page whose component just re-rendered, with the colour shifting as a region updates more often. It is a zero-setup way to see which parts of the screen react to an interaction, but it gives no timings.

open as a page

In React, what does wrapping part of your tree in `<Profiler id="Sidebar" onRender={handleRender}>` actually do — what does it render, and when does React call the onRender callback?

level: juniorimportance: should knowfreq 35%

basics

~20 s

React's Profiler component renders its children unchanged and emits no DOM of its own. Its only job is to call your onRender callback after every commit in which that subtree rendered, passing the id you gave it plus timing numbers for that commit.

open as a page

Inside a React function component you write `const defaultTags = [];` and pass it to a child wrapped in React.memo. Why is that a different array on every render, and what is the simplest way to make it the same array every time?

level: juniorimportance: should knowfreq 42%

basics

~20 s

A function component's body runs again on every render, so the array literal is evaluated again and produces a brand-new array. Move it to a module-level constant outside the component and every render passes the identical array.

open as a page

While measuring render counts in a React application in development, you notice every component's function body runs twice for a single update. Should you treat that as a performance problem?

level: juniorimportance: should knowfreq 48%

basics

~10 s

No. React's StrictMode deliberately calls component functions twice in development to expose impure renders. Production builds render once, so measure a production build before drawing any performance conclusion from the doubling.

open as a page

A React component's body contains `const Chart = lazy(() => import('./Chart'))`, and `<Chart />` is rendered below a search input. Typing in the input makes the Suspense fallback flash and resets the chart's zoom state every keystroke. Why, and what is the fix?

level: middleimportance: should knowfreq 45%

basics

~20 s

Calling lazy during render creates a brand-new component type on every render. React sees a different type at that position, unmounts the old subtree and mounts a fresh one, which loses state and suspends again. Move the lazy call to module scope.

open as a page

A React modal is loaded with React.lazy, so opening it shows a spinner for a beat. How do you make the chunk start downloading when the user hovers or focuses the button that opens it, and why does that not cause the module to load twice?

level: middleimportance: should knowfreq 38%

basics

~20 s

Keep the loader in a named function, pass it to React.lazy, and also call it from the button's onMouseEnter and onFocus handlers. The module registry caches by specifier, so React's later call to the same loader resolves from cache instead of fetching again.

open as a page

Profiling a React screen, one commit in the React DevTools Profiler lasts about 48 ms, yet in that commit's Ranked chart no component's own render time is above 2 ms. What does that pattern tell you, and where do you look next?

level: middleimportance: should knowfreq 44%

basics

~20 s

No single component is slow — the 48 ms is the sum of many cheap renders, so this is a breadth problem, not a hot-component problem. Look at the Flamegraph to see how much of the tree re-rendered and find the highest component in the coloured region.

open as a page

In the React DevTools Profiler, what do the Flamegraph and Ranked charts each show for a single recorded commit, and when would you switch from one to the other?

level: middleimportance: should knowfreq 50%

basics

~20 s

Flamegraph draws a commit as the component tree, where a bar's width covers that component plus its subtree and components that did not re-render are greyed out. Ranked flattens the same commit into a list of components sorted by their own render time.

open as a page

React DevTools has a Profiler setting labelled "Record why each component rendered while profiling". What does enabling it give you, and what does it cost?

level: middleimportance: should knowfreq 46%

basics

~20 s

With that setting on, selecting a component inside a profiled commit shows why it rendered: it was mounting for the first time, specific props changed, its state or hooks changed, context changed, or its parent rendered. The extra bookkeeping makes the profiling session slower.

open as a page

A React code review turns up `const total = useMemo(() => price * quantity, [price, quantity]);`. Is that worth writing? What does the useMemo call itself cost on every render?

level: middleimportance: should knowfreq 48%

basics

~20 s

No. On every render React still allocates the arrow function and the dependency array and compares each dependency, which costs about as much as one multiplication. Write the plain expression instead and save useMemo for expensive work or for a reference that must stay stable.

open as a page

React's `<Profiler>` passes both `actualDuration` and `baseDuration` to its `onRender` callback. What does each one measure, and what do you conclude when the two stay nearly equal on every update?

level: middleimportance: should knowfreq 45%

basics

~20 s

actualDuration is the time React really spent rendering the profiled subtree for this update; baseDuration is the estimated time to render that whole subtree from scratch with nothing skipped. When the two stay nearly equal on updates, almost nothing is being skipped.

open as a page

In React, you move `<ExpensiveTree />` out of a stateful `<Counter>` component and instead pass it into `<Counter>` as `children` from the parent. Why does `ExpensiveTree` stop re-rendering when the counter's state changes, even though nothing is wrapped in React.memo?

level: middleimportance: should knowfreq 55%

basics

~20 s

The children element is created in the parent's render, not inside the stateful component. When only that component's state changes, its children prop is the same object as the previous render, so React bails out and reuses that subtree.

open as a page

In a virtualized React list — react-window's `overscanCount` prop or TanStack Virtual's `overscan` option — what does overscan do, and what do you trade off by raising it from 3 to 30?

level: middleimportance: should knowfreq 42%

basics

~20 s

Overscan renders extra rows just beyond each edge of the viewport. Those buffer rows hide blank flashes during fast scrolling, but each one is real DOM and real render work, so a large overscan gives back the win virtualization bought.

open as a page

Shortly after a deploy, users with a tab open on a React SPA hit a crash when they navigate to a route rendered through React.lazy. At the React level, where does that failure surface, does the Suspense fallback catch it, and what does it take to recover in the UI?

level: seniorimportance: should knowfreq 48%

basics

~20 s

The old tab requests a hashed chunk that the new deploy removed, so the loader's promise rejects and React.lazy throws during render. Suspense handles pending loads only — an error boundary above the lazy component catches it, and recovery normally means reloading the page.

open as a page

React.memo accepts an optional second argument: a function that receives the previous and next props. What must that function return for React to skip the re-render, and what bugs do hand-written comparators commonly introduce?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Return true when the props should be treated as equal, which tells React to skip the re-render — the opposite of the old shouldComponentUpdate convention. Hand-written comparators go wrong by ignoring props that matter, which freezes stale data on screen, or by deep-comparing large objects for more cost than the render saved.

open as a page

Your team wants to ship the `actualDuration` values from React's `<Profiler>` to analytics from real users' browsers. What does a standard React production build do with those measurements, and what does it take to get real numbers?

level: seniorimportance: should knowfreq 30%

basics

~20 s

React disables profiling in its standard production build because the timing instrumentation costs time and memory on every commit. Real production numbers require building against React's profiling-enabled build, which reintroduces that overhead for every user you ship it to.

open as a page

A team enables the React Compiler and deletes their useMemo and useCallback calls. Which performance problems does the compiler still not solve for them?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The React Compiler removes only wasted re-rendering of pure components whose inputs did not change. Genuinely changed inputs, expensive DOM volume, data-fetching waterfalls, bundle size, and slow effects or event handlers all cost exactly what they did before.

open as a page

The React Compiler only produces correct output for code that follows the Rules of React. Which properties does it rely on, and what happens when a component breaks them?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The React Compiler assumes rendering is pure: same inputs produce the same output, and props, state and rendered values are never mutated during render. When it can detect a violation it skips that component and leaves it uncompiled; undetectable violations can cache a stale value and show stale UI.

open as a page

To stop an effect re-running every render, a React codebase writes `useEffect(() => { load(filters); }, [JSON.stringify(filters)]);`, where `filters` is an object rebuilt in the component body each render. When does that work, when does it fail, and what would you do instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

It works only for small plain objects whose keys are always written in the same order. Reordered keys re-run the effect needlessly, and values JSON drops can make genuinely different objects serialize identically. Stabilize the object where it is created instead.

open as a page

showing 1–30 of 41