What are common performance bottlenecks in React apps
Big initial bundle, hydration cost, re-rendering large trees on every state change (no memo, context misuse), expensive renders (heavy list rendering without virtualization, recomputation per render), long synchronous work in handlers (INP), image-heavy LCP, unnecessary effects, memory leaks (no cleanup). Most are addressable with profiling + targeted memoization, virtualization, RSC, and transitions.
Categories
1. Initial load
- Big bundle — every route ships everything. Fix: code split per route, dynamic import for heavy modals/charts.
- Hydration cost — server HTML attaches handlers, runs effects. Fix: RSC ships less JS; partial hydration; islands.
- LCP image — huge unoptimized hero. Fix: AVIF/WebP, responsive srcset, fetchpriority="high".
- Render-blocking CSS/JS — see [[critical-rendering-path-what-blocks-it]].
2. Re-render fan-out
- Context with frequent updates — every consumer re-renders. Fix: split context, or move hot state to Zustand/Redux with selectors.
- Parent re-render cascading — prop changes that trigger child renders even when props are equivalent. Fix:
React.memo+ stable references (useCallback/useMemo) for the actual hot path. - New object/array literals as props —
<Comp data={{...}} />every render. Fix: lift or memoize.
3. Expensive renders
- Long lists — 5k+ DOM nodes. Fix: virtualize (TanStack Virtual, react-window).
- Heavy derivations per render — sort + filter + map on each render. Fix:
useMemo. - Heavy children inside frequent parents — e.g. chart inside a parent that re-renders on hover. Fix: lift, memo, or hoist out.
4. Interaction latency (INP)
- Sync work in handlers — JSON parsing, big array work. Fix: chunk + yield,
startTransition, Web Workers. - Form input lag — controlled input + heavy parent re-render. Fix: localize state,
useDeferredValuefor derived heavy work.
5. Effects gone wild
- Effects firing too often — wrong deps. Fix: tighten deps, use refs for "latest value" patterns.
- No cleanup — listeners, intervals, subscriptions piling up. Fix: return cleanup from
useEffect; AbortController. - Effect-driven fetching that's really server state. Fix: React Query / SWR.
6. Memory leaks
- Listeners not removed on unmount.
- Intervals/timeouts not cleared.
- Detached DOM referenced by long-lived closures.
- Caches that never evict.
7. Network
- Waterfalls — fetch in effect, child fetch in effect, etc. Fix: parallel fetch at load time, RSC for server-side coordination.
- No caching / dedup — N components fetch the same data. Fix: React Query.
- No optimistic UI — every action waits for round-trip.
How I'd debug
- RUM (web-vitals) — INP, LCP, CLS at p75 in prod.
- Lighthouse / DevTools Performance — render timeline, long tasks.
- React DevTools Profiler — flame chart of renders; which committed, why.
- Bundle analyzer — what's actually in the initial JS.
- Memory panel — heap snapshots, detached DOM.
Avoid premature optimization
Most apps don't need a lot of memoization. Profile, find the actual hot path, then apply targeted fixes. Defaults are usually fine.
Interview framing
"Categorize: initial load (bundle, hydration, LCP), re-render fan-out (Context misuse, unstable prop references), expensive renders (long lists, heavy derivations), INP (sync work in handlers), effects (wrong deps, no cleanup), memory leaks, network waterfalls. Default debug toolkit is web-vitals in prod, Lighthouse/Performance panel, React DevTools Profiler, bundle analyzer, Memory panel for leaks. The biggest wins are usually structural (split context, virtualize long lists, RSC for hydration cost, React Query for server state) rather than sprinkling useMemo. And measurement before optimization — most memoization PRs I've seen don't move metrics."
Follow-up questions
- •Walk me through your debug toolkit.
- •When is memoization the right answer?
- •How do you fix context fan-out?
Common mistakes
- •Sprinkling useMemo without measurement.
- •Context with frequent updates.
- •No virtualization on long lists.
- •Server state in client store.
Performance considerations
- •Structural fixes (split state, virtualize, RSC) beat per-component memoization.
Edge cases
- •Suspense boundaries thrashing during streaming.
- •Effects fighting each other.
- •Memo + new object literal prop.
Real-world examples
- •React 18 INP wins, Next.js App Router migration cases, Slack web reflows.