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
- A component re-renders when its state changes, its parent re-renders, or a context it reads changes.
- Re-renders are usually cheap. Optimize when the Profiler shows slow commits or users feel lag.
- Colocate state as low as possible, and lift expensive subtrees out via
children. React.memo,useMemo, anduseCallbackskip work, and the React Compiler applies them automatically.- Virtualize long lists; use stable keys; code-split routes and heavy components with
lazy. - Use
useTransition/useDeferredValueto keep input responsive during expensive updates.
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
- Their own state changed.
- Their parent re-rendered, which by default re-renders all children regardless of props.
- 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
- React DevTools Profiler: records commits and shows which components rendered, why, and how long they took. Enable "Highlight updates when components render" to spot unexpected re-renders.
- Browser Performance panel: long tasks, scripting time, layout, and paint.
- Field data: Core Web Vitals (INP for responsiveness, LCP for loading) from real users.
Memoization
React.memo(Component)skips re-rendering when props are shallowly equal.useMemocaches an expensive computed value;useCallbackcaches a function so memoized children receive the same prop.- Memoization only works if props are stable. Passing a new object literal or inline function each render defeats
memo. - The React Compiler analyzes components at build time and inserts this memoization automatically, which removes most manual
memo/useMemo/useCallback.
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
- React — The library overview
- React Compiler — Automatic memoization at build time
- React Hooks — useMemo, useCallback, and useTransition
- React Suspense — Transitions and lazy loading
- Web Performance — Loading and runtime performance in general
- Core Web Vitals — INP, LCP, and CLS