5. Performance
Performance questions separate people who memorised useMemo from people who have profiled a real app.
1. Virtual DOM
The Virtual DOM is a lightweight JavaScript object tree that mirrors the real DOM.
// What JSX becomes — a plain object
{
type: "div",
props: { className: "box", children: [...] }
}How Rendering Works
- State changes
- React builds a new Virtual DOM tree
- React diffs it against the previous tree
- React computes the minimum set of real DOM operations
- React applies them in one batch (the "commit" phase)
Why This Is Faster
Real DOM operations are expensive — each one can trigger layout recalculation and repaint. Comparing plain JavaScript objects is cheap. React batches many changes into one DOM write.
Interview Point
The Virtual DOM is not inherently faster than hand-written, perfectly optimised DOM code. Its value is that it gives you near-optimal updates from a declarative description, without you tracking what changed. Say this — it is the answer that shows real understanding.
2. Reconciliation and the Diffing Algorithm
Reconciliation is the process of comparing the two trees.
A general tree-diff is O(n³). React gets it to O(n) with two heuristics:
Heuristic 1 — Different element types produce different trees
// Before
<div><Counter /></div>
// After
<span><Counter /></span>React does not try to match them. It destroys the entire old subtree — unmounting Counter and losing its state — and builds the new one from scratch.
Heuristic 2 — Keys identify stable children across renders
{items.map((item) => <Row key={item.id} {...item} />)}With keys, React moves existing DOM nodes instead of recreating them.
Same Type = Update In Place
<div className="before" title="x" />
<div className="after" title="x" />React keeps the DOM node and updates only className. Then it recurses into the children.
Interview Point
The key prop and the "different type = full rebuild" rule are the two things that make the algorithm linear. Both are worth naming explicitly.
3. Fiber
React Fiber (React 16+) is the rewritten reconciler. Its point is interruptible rendering.
| Old (Stack) reconciler | Fiber | |
|---|---|---|
| Rendering | Synchronous, recursive | Incremental, unit-by-unit |
| Interruptible | No | Yes |
| Prioritisation | None | Urgent vs non-urgent updates |
Fiber splits work into small units and can pause after each one to let the browser handle a keystroke or a paint. This is what makes useTransition, Suspense and concurrent features possible.
4. When Does a Component Re-render?
Four causes:
- Its state changed
- Its props changed
- Its parent re-rendered (regardless of props)
- A context it consumes changed
The One People Miss
Cause 3. A parent re-render re-renders all children by default, even children whose props are identical.
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveChild /> {/* re-renders on every click */}
</>
);
}Two Fixes
// Fix 1 — memo
const ExpensiveChild = React.memo(function ExpensiveChild() { /* ... */ });
// Fix 2 — children prop (no memo needed)
function Parent({ children }) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children} {/* created by the grandparent, so unchanged */}
</>
);
}Fix 2 works because children is an element created outside this component. Its reference doesn't change when Parent re-renders. Interviewers rarely expect this one.
5. React.memo
Skips re-rendering when props are shallow-equal to last time.
const UserCard = React.memo(function UserCard({ name, age }) {
return <div>{name} — {age}</div>;
});Custom Comparison
const UserCard = React.memo(Component, (prev, next) => {
return prev.user.id === next.user.id; // true = skip re-render
});Note the inverted logic: return true when the props are equal (skip), unlike shouldComponentUpdate, which returns true to render.
When memo Does Nothing
// A new object every render → shallow compare always fails
<UserCard user={{ name: "Dipak" }} />
// A new function every render → same problem
<UserCard onClick={() => save()} />React.memo only pays off when props are primitives or stable references. Otherwise you added a comparison cost and got zero benefit.
Interview Point
React.memo is shallow. {a: 1} !== {a: 1}. Wrapping every component in memo makes an app slower, not faster.
6. Optimisation Techniques (Full List)
The "name a few ways to optimise a React app" answer:
Rendering
| Technique | What it prevents |
|---|---|
React.memo | Re-render on unchanged props |
useMemo | Recomputing an expensive value |
useCallback | New function identity breaking memo |
Correct key props | Unnecessary unmount/remount |
| State colocation | Keeping state as low in the tree as possible |
| Split contexts | Context consumers re-rendering for unrelated fields |
Bundle
| Technique | Effect |
|---|---|
React.lazy + Suspense | Route-level code splitting |
Dynamic import() | Load heavy libraries on demand |
| Tree shaking | Drop unused exports |
| Bundle analyser | Find the 400 KB date library you forgot about |
Data
| Technique | Effect |
|---|---|
| Debounce / throttle inputs | Fewer requests and renders |
| Pagination or infinite scroll | Smaller payloads |
| React Query caching | Deduplicate identical requests |
| Virtualisation | Render 20 rows, not 10,000 |
Assets
- Lazy-load images (
loading="lazy") - Modern formats (WebP / AVIF)
- Compression (gzip / brotli)
- CDN for static assets
7. State Colocation
The cheapest optimisation, and the one people forget.
// Bad — typing in the modal re-renders the whole page
function Page() {
const [modalInput, setModalInput] = useState("");
return (
<>
<HugeTable />
<Modal value={modalInput} onChange={setModalInput} />
</>
);
}
// Good — state lives where it is used
function Modal() {
const [input, setInput] = useState("");
}Move state down, not up, whenever only one subtree needs it. No memo required.
8. Code Splitting
import { lazy, Suspense } from "react";
const Chart = lazy(() => import("./Chart"));
function Dashboard() {
return (
<Suspense fallback={<Skeleton />}>
<Chart />
</Suspense>
);
}lazy() takes a function returning a dynamic import(). The bundler emits a separate chunk, fetched only when the component first renders.
Suspense
<Suspense> declares a fallback for anything below it that is not ready — a lazy component, or (in React 19) a promise read with use().
<Suspense fallback={<Spinner />}>
<Comments commentsPromise={promise} />
</Suspense>Interview Point
Best places to split: route boundaries, modals, charts, rich-text editors, and anything below the fold. Do not split tiny components — the extra network request costs more than the bytes saved.
9. List Virtualisation
Rendering 10,000 rows creates 10,000 DOM nodes and freezes the browser. Virtualisation renders only what is visible plus a small buffer.
import { useVirtualizer } from "@tanstack/react-virtual";
function List({ rows }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((row) => (
<div
key={row.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${row.start}px)`,
height: row.size,
}}
>
{rows[row.index].name}
</div>
))}
</div>
</div>
);
}Libraries: @tanstack/react-virtual, react-window, react-virtuoso.
10. Debouncing and Throttling
| Debounce | Throttle | |
|---|---|---|
| Behaviour | Wait until activity stops | Run at most once per interval |
| Use for | Search input, autosave | Scroll, resize, mousemove |
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 400);
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}The input stays fully responsive; only the network call is delayed.
11. React Compiler
Stable as v1.0 since October 2025. A build-time compiler that automatically memoises components and values.
// You write this
function Cart({ items }) {
const total = items.reduce((s, i) => s + i.price, 0);
return <Total value={total} />;
}
// The compiler emits the equivalent of useMemo/useCallback/memoWhat It Changes
- Manual
useMemo/useCallback/React.memobecome largely unnecessary - Your code must follow the Rules of React — no mutation during render, no side effects in the render body
eslint-plugin-react-compilertells you where you break those rules
Interview Point
The Compiler does not make memoisation obsolete as a concept — you still need to explain why re-renders happen. It automates the mechanical part. Mentioning it signals you follow current React, which lands well in 2026 interviews.
12. Measuring Before Optimising
React DevTools Profiler
Records a session and shows which components rendered, how long each took, and why.
Profiler Component
<Profiler
id="Cart"
onRender={(id, phase, actualDuration) => {
console.log(id, phase, actualDuration);
}}
>
<Cart />
</Profiler>Highlight Updates
DevTools → Settings → "Highlight updates when components render". Flashing borders show you exactly what is re-rendering as you click around.
Interview Point
"I would profile first" is the correct opening to any optimisation question. Premature memoisation adds complexity and memory pressure while often making things slower.
13. SSR vs CSR
| CSR | SSR | |
|---|---|---|
| HTML from server | Empty shell | Fully rendered |
| First contentful paint | Slow | Fast |
| Time to interactive | Faster after load | Needs hydration |
| SEO | Weaker | Strong |
| Server cost | Low | Higher |
| Navigation after load | Instant | Instant (after hydration) |
Hydration
The server sends HTML. The client downloads the JavaScript and attaches event listeners to the existing markup — it does not re-create the DOM.
// Client entry for SSR
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);Hydration Mismatch
If the server HTML and the first client render differ, React warns and may discard the server markup. Usual culprits: Date.now(), Math.random(), window, localStorage, and locale-dependent formatting.
// Fix: render the client-only value after mount
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;Interview Point
SSR improves perceived performance and SEO. It does not reduce the JavaScript bundle. Reducing the bundle is what React Server Components do — see Next.js.
14. Styling Options
Grouped here because "how do you style a React component" is usually asked alongside performance.
| Approach | Example | Trade-off |
|---|---|---|
| Inline styles | style={{ color: "red" }} | No pseudo-classes, no media queries |
| CSS file | import "./App.css" | Global scope, name collisions |
| CSS Modules | import s from "./A.module.css" | Scoped, zero runtime, build step |
| Tailwind | className="flex gap-4" | Fast to write, verbose markup |
| CSS-in-JS | styled-components, Emotion | Dynamic styling, runtime cost, poor RSC support |
| Zero-runtime CSS-in-JS | vanilla-extract, Panda | Dynamic API, extracted at build |
Interview Point
CSS-in-JS libraries with a runtime do not work in Server Components (they need React context and browser APIs). That is why the ecosystem moved toward Tailwind, CSS Modules and zero-runtime solutions.