React

React Performance Optimization👨‍💻

Most React apps are fast enough without any optimization. The real skill is knowing when performance actually matters and reaching for the right tool instead of wrapping everything in memo and hoping for the best. This guide covers the techniques that make a measurable difference in production -- from memoization and virtualization to the React Compiler that automates most of it for you.

Key Takeaways

  • 1Profile before you optimize. React DevTools Profiler shows exactly which components re-render and how long they take -- guessing wastes your time and adds complexity for nothing.
  • 2React.memo skips re-renders when props haven't changed (shallow comparison). It only helps when combined with stable references via useMemo and useCallback for object/array/function props.
  • 3useMemo caches expensive derived data (filtering, sorting, transformations). Skip it for trivial calculations -- `price * quantity` does not need memoization.
  • 4useCallback stabilizes function references so memoized children don't re-render. It's pointless if the child isn't wrapped in memo.
  • 5The React Compiler (React 19) analyzes your components at build time and inserts memoization automatically. Write clean, idiomatic code and let the compiler handle the optimization.
  • 6Virtualization (rendering only visible items) is the only real fix for long lists. No amount of memoization helps when you have 5,000 DOM nodes.

Master react performance optimization

Take the React Performance Deep Dive course with hands-on lessons and challenges.

Examples

memo with custom comparator -- skip re-renders on deep data

tsx

The default shallow comparison fails when the parent rebuilds the order object on every render. A custom comparator checks only the fields the component actually displays, avoiding unnecessary re-renders without requiring the parent to memoize the entire object.

useMemo for expensive filter + sort pipeline

tsx

Filtering and sorting 10,000 products on every keystroke is expensive. useMemo ensures the pipeline only reruns when the inputs (products, search, sortBy, minRating) actually change. Typing in an unrelated input field won't retrigger this computation.

useCallback for stable event handlers passed to memoized list items

tsx

Without useCallback, handleToggle and handleDelete are new function instances on every render, which means every TaskItem re-renders even though memo wraps it. useCallback preserves the same function reference, so only the TaskItem whose task prop actually changed will re-render.

Virtualized list with TanStack Virtual -- 10k rows, 60fps scroll

tsx

Rendering 10,000 log entries as DOM nodes freezes the browser. TanStack Virtual renders only the ~20 visible rows plus a 10-row overscan buffer. The user scrolls at 60fps because the DOM node count stays constant regardless of dataset size.

Code splitting with lazy() -- load heavy routes on demand

tsx

Route-based code splitting is the highest-impact optimization for initial load time. Each page becomes its own JS chunk. The NavLink component prefetches the chunk on hover, so by the time the user clicks, the code is already cached. This is the simplest way to cut your initial bundle in half.

React Compiler -- write plain code, get automatic memoization

tsx

The React Compiler (available in React 19) analyzes your components at build time and inserts memo, useMemo, and useCallback where beneficial. You write straightforward code without manual optimization, and the compiler does the work. Use 'use no memo' to opt out components that violate the Rules of React (mutations during render, side effects in the render path). For Next.js, it's a single config flag.

Common Mistakes

Mistake:

Wrapping every component in memo without profiling first. Developers add memo defensively across the entire codebase, creating overhead from shallow-comparison checks on components that render cheaply or always receive new props anyway.

Fix:

Profile with React DevTools Profiler. Only apply memo to components that (1) re-render frequently, (2) are expensive to render, and (3) receive props that can realistically be stabilized. A component that always gets new children or inline objects from its parent won't benefit from memo.

Mistake:

Using useCallback without a memoized child consuming it. If the function is passed to a regular (non-memoized) component, wrapping it in useCallback adds overhead with zero benefit since the child re-renders regardless.

Fix:

useCallback only prevents re-renders when paired with memo on the child. If the child isn't memoized, skip useCallback. The exception is when the callback appears in a useEffect dependency array, where a stable reference prevents the effect from re-running.

Mistake:

Putting all application state into a single React context. Every state change re-renders every consumer -- even consumers that only read a different slice of the context value.

Fix:

Split contexts by update frequency. Put user/auth data (rarely changes) in one context and real-time data (frequently changes) in another. For fine-grained subscriptions, use external state libraries like Zustand or Jotai that support selectors out of the box.

Mistake:

Memoizing trivial computations with useMemo. Wrapping `count * 2` or string concatenation in useMemo adds the cost of dependency comparison and cache storage for something that takes microseconds to compute.

Fix:

Reserve useMemo for genuinely expensive work: filtering/sorting large arrays, complex formatting pipelines, or creating objects that serve as stable references for memoized children. Inline arithmetic and string operations should be computed directly.

Best Practices

  • Profile first, optimize second. Open React DevTools Profiler, record an interaction, and look at the flame graph. Fix the components that are actually slow, not the ones you assume might be slow.
  • Prefer composition over memoization. Moving state closer to where it's used (state colocation) or passing children as props often eliminates unnecessary re-renders without any memo, useMemo, or useCallback.
  • Virtualize lists over 200-500 items. No memoization strategy fixes the cost of 5,000 real DOM nodes. Use TanStack Virtual or react-window to render only what's visible.
  • Split your bundle with lazy() and Suspense. Route-based code splitting is the single highest-impact optimization for initial load time. Prefetch chunks on hover for instant perceived navigation.
  • Adopt the React Compiler when upgrading to React 19. It eliminates most manual memoization. Write clean, idiomatic components that follow the Rules of React (no mutations during render, no side effects in the render path) and let the compiler handle optimization.
  • Stabilize context values with useMemo. If your context provider creates a new object on every render, every consumer re-renders. Memoize the value object and split state from actions into separate contexts.

Summary

React performance optimization is about knowing when to act and picking the right tool. Start by profiling with React DevTools -- most components don't need optimization. When they do, reach for memo (components), useMemo (expensive computations), and useCallback (stable function references for memoized children). Virtualize long lists with TanStack Virtual, split bundles with lazy(), and structure your component tree so state changes don't cascade. With React 19, the React Compiler automates most memoization at build time, letting you write clean code without manual optimization boilerplate.

Practice React with hands-on challenges

Learn react performance optimization hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master React with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.