Back to React
React
medium
mid

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.

9 min read·~5 min to think through

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

tsx
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'

  1. Route-level code splitting — usually 40–60% bundle reduction.
  2. Image optimization — LCP cut in half.
  3. Removing a heavy dep (moment, lodash, jquery) — 50–200 KB.
  4. Virtualizing the biggest list — frame rate jumps from 20fps to 60fps.
  5. 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.

Senior engineer discussion

Senior framing: lead with measurement. The wrong optimization is often expensive — adding memoization where it doesn't help, splitting bundles that load together anyway.

Related questions