React Performance

React is fast by default for most interfaces, but large component trees, long lists, frequent state updates, and heavy JavaScript bundles can make apps feel sluggish: typing lags, scrolling stutters, and pages take seconds to become interactive. Performance problems in React usually fall into two buckets: rendering too much (unnecessary re-renders and expensive render work) and shipping too much (large bundles that delay loading and hydration).

The most effective approach is measure first. Profile, find the actual bottleneck, and apply the targeted fix, whether that's colocating state, virtualizing a list, splitting a bundle, or marking an update as a transition. Blanket memoization everywhere rarely helps and often hurts readability.

TL;DR

Quick Example

A slow filtered list made responsive: virtualization plus a deferred value.

Instead of rendering 20,000 rows, React renders the ~20 visible ones, and filtering happens at lower priority than keystrokes.

Core Concepts

Render and Commit

When state changes, React renders: it calls your component functions to produce a new element tree. It then reconciles it against the previous tree and commits only the DOM changes needed. A re-render that produces identical output costs JavaScript time but no DOM work. Slowness comes from rendering many components, doing expensive work during render, or committing large DOM changes.

Why Components Re-render

  1. Their own state changed.
  2. Their parent re-rendered, which by default re-renders all children regardless of props.
  3. A context they consume changed value.

Props changing doesn't cause a re-render; the parent re-rendering does. That's why moving state down, or passing components as children, is so effective.

Measuring

Memoization

Techniques by Problem

Keys

Keys tell React which list items are the same across renders. Use stable, unique IDs. Index keys cause unnecessary remounts and state bugs when items are inserted, removed, or reordered, and random keys (Math.random()) remount every item on every render.

Transitions

startTransition(() => setFilter(value)) marks an update as non-urgent: React keeps urgent updates such as typing responsive, and can interrupt the transition render if new input arrives. useDeferredValue(value) achieves the same for a value you receive. Both improve INP without reducing the work itself. See React Suspense.

Code Splitting

Split by route first, then by heavy, rarely used components (charts, editors, maps). Frameworks like Next.js split routes automatically.

Best Practices

Profile Production Builds

Development builds include extra checks and are much slower. Profile with a production build (React supports a profiling build) and on realistic, lower-end devices.

Move State Down, Lift Content Up

Keep state in the component that uses it. When a stateful wrapper must contain expensive content, accept that content as children. Children created by the parent's parent don't re-render when the wrapper's state changes.

Keep Render Pure and Cheap

Avoid heavy computation, object allocation in hot loops, and synchronous storage or DOM reads during render. Precompute on the server, memoize, or move work into event handlers.

Watch Bundle Size

Analyze bundles (vite-bundle-visualizer, @next/bundle-analyzer), prefer lighter dependencies, import only what you use, and keep third-party scripts off the critical path. See web performance.

Common Mistakes

Memoizing With Unstable Props

One Giant Context

A single context holding user, theme, cart, and notifications re-renders every consumer whenever any of them changes. Split by concern and update frequency. See React Context.

Optimizing Without Measuring

Wrapping every component in memo adds comparison overhead and complexity, and often fixes nothing because the real bottleneck is a long list, a slow effect, or a bloated bundle.

FAQ

Are re-renders bad?

Not inherently. React is designed to re-render often, and most re-renders take microseconds. They become a problem when they're expensive (large subtrees, heavy computation) or frequent enough to cause visible lag. Measure before optimizing.

Should I wrap every component in React.memo?

No. memo helps only when a component re-renders often with the same props and its render is costly. Otherwise the prop comparison is wasted work. With the React Compiler, manual memo is rarely needed at all.

What's the best way to render a list of 10,000 items?

Virtualize it: render only visible rows with TanStack Virtual or react-window. Combine with pagination or infinite loading on the data side, and stable keys.

How do I improve INP in a React app?

Find slow interactions with the Profiler and field data, then reduce work on the interaction path: colocate state, virtualize, memoize expensive renders, move non-urgent updates into transitions, and break up long tasks. Reducing JavaScript shipped and hydrated also helps a lot.

Related Topics

References