How do you optimize React applications for performance?
Layered: (1) measure with Profiler + Lighthouse; (2) ship less JS — code split per route, lazy-load heavy components, prune deps; (3) render less — stable keys, React.memo on hot rows, useMemo/useCallback for stable identities, split contexts; (4) defer non-urgent work with startTransition / useDeferredValue; (5) virtualize long lists; (6) optimize images/fonts; (7) cache server state with React Query. Profile FIRST.
Performance work is layered. Measure, fix the biggest thing, measure again.
0. Measure first
- React DevTools Profiler — what's re-rendering and how often.
- Lighthouse — Core Web Vitals (LCP, INP, CLS), bundle audit.
- Chrome DevTools Performance — main-thread time, scripting vs rendering.
- Bundle analyzer (Vite/Next plugin) — what's in the bundle.
Without measurement, every optimization is a guess.
1. Ship less JavaScript
- Route-level code splitting: lazy(() => import('./Page')) per route.
- Lazy load heavy components: charts, editors, modals — only when used.
- Prune deps: replace moment with date-fns, lodash with native, etc.
- Preload critical routes: link rel=preload on hover/idle.
- Tree shake: import only what you use.
- Bundle budgets in CI: 200 KB gzipped initial, 100 KB per route.
2. Render less
- Stable keys — item.id, not index.
- React.memo on rows + leaf components inside hot trees.
- useCallback / useMemo for props passed to memoized children.
- Split contexts — separate read-rarely from read-often.
- Move state DOWN — keep state in the smallest subtree that needs it.
- Lift state UP only when siblings need to share.
3. Defer non-urgent work
const [pending, startTransition] = useTransition();
function onSearch(q: string) {
setQuery(q);
startTransition(() => {
setResults(filter(q));
});
}4. Virtualize long lists
Over 500 items: react-window / TanStack Virtual. Render only what's visible.
5. Optimize images and fonts
- AVIF/WebP with picture or framework-managed (next/image).
- Lazy-load below-the-fold images (loading=lazy).
- woff2 fonts, font-display swap, subset to needed characters.
- Preload the LCP image.
6. Server state caching
React Query / SWR caches by key, deduplicates concurrent requests, refetches in background. Doing this manually is where 'fast' apps slow down.
7. SSR / SSG / RSC
- Static pages: SSG.
- Dynamic pages: SSR with streaming.
- Mostly-static with bits of interactivity: RSC + Client Components.
8. Other levers
- CDN delivery: edge caching for static + HTML.
- HTTP/2 / HTTP/3: multiplex requests, smaller header overhead.
- CSS strategy: avoid runtime CSS-in-JS for hot paths; prefer Tailwind/CSS Modules.
- Reduce re-layouts: batch DOM reads, avoid forced reflow.
- Web Workers for heavy CPU (parsing, search indexing).
Anti-patterns
- Memoizing everything — bookkeeping costs more than saved renders.
- Optimizing components that render in 0.2ms.
- Pulling in a state library to fix a useEffect bug.
- Caching aggressively without invalidation.
Common 'biggest wins'
- Route-level code splitting — usually 40–60% bundle reduction.
- Image optimization — LCP cut in half.
- Removing a heavy dep (moment, lodash, jquery) — 50–200 KB.
- Virtualizing the biggest list — frame rate jumps from 20fps to 60fps.
- React Query — replaces N ad-hoc fetches + caches.
Interview framing
I always measure first — Profiler for render cost, Lighthouse for delivery. Then I fix the biggest thing: usually bundle size, then image optimization, then a hot render path. Memoization is a last resort, not a first move.
Follow-up questions
- •What's the biggest performance win you've shipped?
- •When is React.memo a bad idea?
- •How do you set up bundle budgets in CI?
Common mistakes
- •Memoizing without measuring — pure overhead.
- •Optimizing renders when the actual cost is on the network.
- •Adding a CDN without setting cache headers correctly.
Performance considerations
- •Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1. Initial bundle should target < 200 KB gzipped for content-heavy pages. Profile under throttled network + slow CPU.
Edge cases
- •Memoization breaks down when a parent re-creates props every render.
- •Virtualization can hide content from search/scroll-restore.
- •Service workers can serve stale assets after deploy — version your cache.
Real-world examples
- •Pinterest's PWA case study (cutting bundle in half doubled engagement). Web.dev case studies (Pinterest, Tinder, Twitter Lite) follow the same shape: measure, ship less JS, optimize delivery.