React Performance in the Real World

How to actually triage performance in a production app — finding the right problem before reaching for the right tool.

March 22, 20264 min read5 / 5

All the patterns in this series are answers to specific questions. Before applying them, you need to find the right question. Optimizing the wrong thing is worse than not optimizing at all — it adds complexity without moving the metric that matters.

Here's how I think about approaching performance in a real app.

Find the Highest-Agony Bottleneck First

A useful mental model for prioritization: agony = slowness × traffic.

A page that takes 3 seconds to load but only 10 people visit per month is low agony. A search input that lags 200ms on every keystroke for thousands of daily users is high agony. Start there.

Some areas that are consistently high agony:

  • Initial load — if the app takes more than a second to become interactive, fix that before anything else
  • Frequent interactions — anything the user does repeatedly: typing, clicking filters, toggling items
  • The things that feel sticky — load a page, use it for 5 minutes, notice it getting gradually slower. That's usually a memory or event listener leak, not a render problem.

The less obvious problems are often the ones nobody owns. Large enough apps have multiple teams, and the areas of most pain tend to fall between team boundaries. Getting alignment on who fixes a cross-cutting problem is sometimes the harder job.

Use the Tools Before Reaching for the Optimizations

The two tools that actually tell you what's happening:

React DevTools — Highlight Updates: Turn on "Highlight updates when components render." Anything flashing when it shouldn't be is a problem. Anything flashing slowly is worth investigating. Anything flashing at 1.1ms is not worth your time — leave it alone.

Chrome DevTools — Performance tab: Hit Record, interact with the app, stop. Look at:

  • CPU usage — sustained 100% during an interaction is a red flag
  • Long tasks — Chrome puts a red flag on anything exceeding the 16.6ms frame budget
  • Event handlers — if a click handler takes 800ms, that's where to start

The insight from the tools usually tells you what is slow. The patterns in this series tell you how to fix it.

Match the Tool to the Problem

What you observeLikely fix
Component re-renders when its props/state didn't changePush state down, or React.memo + useCallback/useMemo
Expensive calculation re-runs when inputs are the sameuseMemo
Typing/clicking makes the whole UI feel stickyuseTransition
Results lag but input feels fineAlready using transitions correctly
Slow network response feels jarringuseOptimistic
Too many DOM nodes (thousands of list items)Virtualization / windowing
Initial load is slowCode splitting, Suspense for data fetching, server rendering

The table oversimplifies, but it's a useful first filter. If the problem isn't in the table, measure more before guessing.

Don't Memoize Everything

The most common mistake after learning about React.memo, useCallback, and useMemo is applying them everywhere preemptively. It feels productive. It's usually harmful.

React.memo prevents renders when props are the same — but if a component's state changes (via a hook), it still re-renders regardless of the memo wrapper. Wrapping everything in memo and having it silently not work is confusing to debug.

More importantly: memoization on its own can break things. If a grandchild component needs an update that should have cascaded from a parent, a React.memo in the middle can silently swallow it. A slow app that works correctly beats a fast app that shows stale data.

React Compiler is the principled answer to "why can't memo just be automatic." It analyzes the code at build time and understands the dependency graph — something runtime React.memo can't do. For greenfield projects, enabling it from the start is worth considering. For existing codebases, the incremental migration path is worth reading about in the React Compiler post.

The Cost of Over-Engineering

There's a version of this that I've seen go wrong: spending a week memoizing everything in a component that turns out nobody uses, or adding useTransition to a search that runs in 2ms and was never a problem.

The goal is shipping software that works well for users — not having the most theoretically optimal render tree. If a re-render completes in under a millisecond, it's invisible to users and not worth your time.

A practical standard: if a user-initiated interaction consistently takes more than 100ms from click to visual feedback, investigate it. Below that, most users won't notice. Above it, the tools will show you exactly where the time is going.

Applying This Progressively

For a new codebase:

  1. Enable React Compiler from the start — get memoization coverage without manual work
  2. Use useTransition for any interaction that drives an expensive UI update
  3. Use useOptimistic for mutations that should feel instant
  4. Profile before shipping if anything feels slow

For an existing codebase:

  1. Find the highest-agony bottleneck with DevTools
  2. Apply the targeted fix — don't refactor the whole component
  3. Measure before and after
  4. Move to the next bottleneck

The hardest truth about React performance: most of the time, the biggest wins come from structural decisions — where state lives, how the component tree is shaped — not from sprinkling optimization hooks. Push state down before adding memoization. Memoize before adding transitions. Transitions before virtualization.

The order matters because each step is riskier and more complex than the previous one. Start with the simplest intervention that could possibly work.

Practice what you just read.

Diagnose and Fix Three Common Performance Problems
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.