Module 12 · Performance & Errors
Module 12 · Questions 64–67, 72–79

Performance & Error Handling

The senior module. Notice how often the right answer starts with "measure first" — that's exactly what they're listening for.

Q64 · Q65 · Q66

Error Boundaries

In one line

A class component that catches errors thrown while rendering its children and shows a fallback UI instead of a blank white screen.

Without one, a single error anywhere unmounts the entire React tree — the user sees a blank page and has no idea what happened.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };            // 1. switch to the fallback UI
  }

  componentDidCatch(error, info) {
    logToServer(error, info.componentStack);   // 2. report it
  }

  render() {
    if (this.state.hasError) return <h3>Something went wrong. <button onClick={...}>Retry</button></h3>;
    return this.props.children;
  }
}

<ErrorBoundary><Dashboard /></ErrorBoundary>

Q65 · What they catch — and what they don't

✅ Caught❌ NOT caught
Errors during renderEvent handlers (onClick) — use try/catch
Errors in lifecycle methodsAsync code — setTimeout, promises, API calls
Errors in child constructorsServer-side rendering
Errors thrown in the boundary itself
Q66 · The trick question

"Do error boundaries replace try/catch around API calls?"  No. An API call is asynchronous — by the time the promise rejects, the render is long finished, so the boundary never sees it. API errors need try/catch and an error state in the UI (Q80). The boundary is the last-resort net for rendering crashes, like user.name when user is null.

Where to put them

Not one giant boundary around the whole app — then any error still blanks everything. Wrap each route, plus risky widgets like charts and third-party embeds, so a broken dashboard tile doesn't take the navigation down with it. In function components you use the react-error-boundary package, since the feature itself still requires a class.

30-second interview answer

An error boundary is a class component using getDerivedStateFromError and componentDidCatch that catches errors thrown while rendering its children and shows a fallback instead of unmounting the whole tree. It catches render, lifecycle and constructor errors — not event handlers, not async code, not itself — so it doesn't replace try/catch around API calls, which are asynchronous and need their own error state. I wrap each route and any risky widget separately, and log the error with the component stack so it can be traced.

Q75 · Q76

Why are keys required in lists — and why not the array index?

In one line

A key gives each item a stable identity so React can match it across renders. An index isn't an identity — it changes when the list changes.

WITH STABLE IDs WITH INDEX AS KEY insert "New" at the top insert "New" at the top old: [id1, id2, id3] old: [0, 1, 2] new: [id9, id1, id2, id3] new: [0, 1, 2, 3] React: "id9 is new" → inserts 1 React: "0 changed, 1 changed, DOM node 2 changed, 3 is new" → rewrites EVERY row, and row state moves to the wrong item

The visible bug: type into the first row's input, insert a new item at the top, and your text is now attached to the wrong row — because React reused DOM node 0 for a different item.

Tick the box next to "Brands" in both lists, then press Add at top. In the index-keyed list the tick jumps to the new row; in the id-keyed list it stays with "Brands".

The rules

  • Keys must be unique among siblings, not globally.
  • Use a stable id from the data — a database id, not Math.random(), which changes every render and forces a full rebuild.
  • Index is acceptable only when the list is static, never reordered, filtered or added to.
  • The key goes on the outermost element inside map, and it isn't readable as a prop.
30-second interview answer

Keys give list items a stable identity so React's reconciliation can match each element to the same item between renders instead of matching by position. With the array index as the key, inserting or reordering shifts every index, so React thinks every row changed — it rewrites DOM it didn't need to, and worse, component state and input values stay attached to the position rather than the item, so they end up on the wrong row. Index keys are only safe for a static list that's never reordered or filtered.

Q72 · Q73 · Q74

Lazy loading, code splitting and Suspense

In one line

Code splitting breaks the bundle into chunks, lazy loading downloads a chunk only when it's needed, and Suspense shows a fallback while it arrives.

const ReportsPage = React.lazy(() => import("./pages/ReportsPage"));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/reports" element={<ReportsPage />} />
  </Routes>
</Suspense>
WITHOUT splitting WITH route-based splitting one bundle: 2.4 MB main: 380 KB ← loaded at startup every user downloads brands: 90 KB ← only when visiting /brands every page, even ones reports: 310 KB ← only when visiting /reports they never open editor: 640 KB ← only when actually used
TermWhat it isWho does it
Code splittingSplitting JS into chunksThe bundler (Vite/webpack), triggered by import()
Lazy loadingLoading a chunk on demandReact.lazy
SuspenseThe loading UI while it's fetchedReact
What to split, in order of value

1. Routes — the biggest win, one line each. 2. Heavy libraries — a chart library, a rich-text editor, a PDF viewer; don't ship a 600 KB editor to users who never open it. 3. Modals and dialogs that are rarely opened. Also pair a lazy route with an error boundary, because a failed chunk download throws.

30-second interview answer

Code splitting is the bundler breaking the JavaScript into separate chunks, triggered by a dynamic import(). React.lazy wraps that so a component is only downloaded when it's first rendered, and Suspense provides the fallback UI while the chunk is in flight. The standard use is per route, so the initial bundle only contains what's needed for the first screen, plus splitting heavy libraries like chart or editor packages. I'd wrap lazy routes in an error boundary too, since a failed chunk load throws during render.

Q77 · Q78

Optimising large lists (and what FlatList is)

In one line

Virtualization — render only the rows currently visible, not all ten thousand.

10,000 rows rendered virtualized (react-window) 10,000 DOM nodes ~20 DOM nodes slow first paint instant laggy scrolling smooth huge memory flat memory ┌─────────────┐ ┌─────────────┐ │ visible │ │ visible │ ← only these exist ├─────────────┤ └─────────────┘ │ 9,980 rows │ rows swap in/out as you scroll │ nobody sees │ └─────────────┘
import { FixedSizeList } from "react-window";

<FixedSizeList height={600} itemCount={items.length} itemSize={48} width="100%">
  {({ index, style }) => <div style={style}>{items[index].name}</div>}
</FixedSizeList>

The full checklist

  1. Paginate on the server first — the cheapest fix is not sending 10,000 rows.
  2. Virtualize with react-window or TanStack Virtual when you genuinely need a long scroll.
  3. Stable keys — database ids, never the index (Q76).
  4. Memoize the row with React.memo + useCallback for its handlers.
  5. No inline objects or arrow functions in the row props — they break the memo.
  6. Debounce search and filter inputs (Q98).

Q78 · FlatList is React Native's built-in virtualized list — the same idea, but mobile. It takes data, renderItem and keyExtractor, and renders only what's on screen. Native has no DOM, so it ships virtualization in the box; on the web you add react-window yourself.

30-second interview answer

First I'd avoid the problem by paginating on the server. If a long list is genuinely needed, I virtualize it with react-window so only the visible rows exist in the DOM — that turns ten thousand nodes into about twenty and keeps scrolling smooth. On top of that: stable database ids as keys, the row wrapped in React.memo with useCallback handlers so it doesn't re-render when the parent does, and no inline objects in the props which would break that memo. In React Native the equivalent is FlatList, which is virtualized by default.

Q67 · Q79

How do you optimise React performance and debug a slow app?

In one line

Measure first. Find where the time actually goes, fix the cause, and only then memoize.

Step 1 — Measure (Q79)

ToolTells you
React DevTools ProfilerWhich components render, how often, how long, and why ("Why did this render?")
"Highlight updates" settingFlashing borders showing what re-renders as you interact — the fastest way to spot waste
Network tabSlow endpoints, request waterfalls, a list page firing one call per row (the frontend N+1)
Lighthouse / bundle analyzerBundle size, unused libraries, images, slow first paint
Say this line

"Most React 'performance problems' turn out to be one of three things: a slow API, rendering too much data at once, or re-rendering a large tree on every keystroke. Memoization is usually the last fix, not the first."

Step 2 — Fix the cause

  1. Move state down. If a search input at the top of a page re-renders the whole table, push that state into a smaller component.
  2. Fix keys. Index keys cause whole-list rebuilds.
  3. Stop creating objects in the parent that break children's memo.
  4. Memoize the expensive bits — React.memo on heavy children, useMemo on real calculations, useCallback for their handlers.
  5. Virtualize long lists, debounce inputs.
  6. Code-split routes and heavy libraries, and lazy-load images.
  7. Cache server data with React Query instead of refetching on every mount.
30-second interview answer

I start by measuring, not guessing. The React Profiler with "highlight updates" shows what re-renders and why, the Network tab shows slow endpoints and request waterfalls, and a bundle analyzer shows what's bloating the first load. Then I fix the cause rather than papering over it — usually state sitting too high in the tree, index keys rebuilding lists, or objects recreated in a parent breaking a child's memo. After that come the tools: React.memo and useMemo where the profiler showed cost, virtualization for long lists, debouncing for search inputs, route-level code splitting, and React Query so data isn't refetched on every mount. In my experience most 'React is slow' issues are really a slow API or rendering far too many rows at once.