How to Improve React App Performance
Published July 16, 2026 · 10 min read
Performance work is most useful when it changes something a person can feel: an interaction responds sooner, scrolling stays smooth, or the page becomes usable with less waiting. A codebase is not fast because it contains memo. It is fast because it avoids unnecessary work on the critical path and proves the result with evidence.
Treat every optimization as a hypothesis: identify the slow experience, change one cause, and measure again.
Establish a baseline before changing code
Start with one user-visible problem rather than a general goal to “make React faster.” Record the device, route, data size, and interaction that reproduces it. Then collect evidence from the browser Performance panel and the React Developer Tools Profiler.
The Profiler answers React-specific questions: which components rendered, how long a commit took, and why work repeated. Browser tooling shows the larger system around React, including network requests, JavaScript evaluation, layout, paint, and long tasks.
| Symptom | Useful evidence | First place to look |
|---|---|---|
| Typing feels delayed | Profiler commits during each keystroke | State placement and expensive derived data |
| A dialog opens slowly | Network waterfall and loaded chunks | Code splitting and request sequencing |
| Scrolling stutters | Long tasks, layout, and paint activity | Large lists, layout work, and heavy rows |
| Development renders appear twice | Development build with Strict Mode | Confirm production behavior before optimizing |
Remove unnecessary render work
React must re-render after relevant state changes. The goal is not zero renders; it is to keep each update scoped and inexpensive.
Keep transient state close to where it is used
Do not lift hover state, draft form values, or an expanded row to the application root unless distant consumers genuinely need it. A state update re-renders the component that owns it and the descendants React needs to reconcile. Local ownership naturally narrows that work.
Prefer component composition as well. When a wrapper accepts stable JSX through children, its local update does not require recreating every child description.
Derive values during render
An Effect that calculates state from other state creates an extra render with a stale value followed by another render with the result. Calculate cheap derived values directly. Cache only calculations that profiling shows are meaningfully expensive.
import { useMemo } from "react"
type Product = {
id: string
name: string
category: string
}
export function ProductList({
category,
products,
}: {
category: string
products: Product[]
}) {
const visibleProducts = useMemo(
() => products.filter((product) => product.category === category),
[category, products]
)
return (
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}For a small array, remove useMemo and filter during render. The memo becomes valuable only when the calculation is expensive enough, its dependencies are stable, and the component renders frequently. React’s guide on avoiding unnecessary Effects is a strong checklist for this class of problem.
Keep rendering pure
Rendering should calculate JSX from props, state, and context without mutating external values or starting side effects. If an additional render breaks a component or produces a visible artifact, fix the impurity instead of hiding it behind memoization.
Use memoization deliberately
memo, useMemo, and useCallback are targeted tools. They can skip demonstrated work or stabilize a value whose identity is part of another optimization. They also add dependency management and comparison work, so applying them everywhere can make code harder to reason about without improving the experience.
Use manual memoization when all three statements are true:
- Profiling identifies a costly repeated render or calculation.
- The inputs remain equal often enough to skip that work.
- The saved work is greater than the memoization overhead.
This project enables React Compiler. Current React guidance is to let the compiler handle routine component and value memoization in new code, then use manual APIs when precise identity control is still required. Existing manual memoization should remain until profiling and regression tests prove it is safe to remove. Compiler diagnostics may also skip unsupported components while optimizing the rest of the application.
Read the current React Compiler guidance before turning old memoization rules into team-wide policy.
Split code at meaningful interaction boundaries
The fastest JavaScript is the JavaScript a user does not need yet. Split large editors, charts, admin tools, and infrequently opened dialogs at interaction boundaries. Keep the fallback close to the delayed content so the rest of the page remains stable.
import { lazy, Suspense } from "react"
const RevenueChart = lazy(() => import("./RevenueChart"))
export function AnalyticsPanel({
showChart,
}: {
showChart: boolean
}) {
return (
<section>
<h2>Revenue</h2>
{showChart ? (
<Suspense fallback={<p>Loading chart…</p>}>
<RevenueChart />
</Suspense>
) : null}
</section>
)
}Do not split every small component. Excessive chunks increase request overhead and can create a sequence of fallbacks. Split where the deferred code is substantial and not required for the initial experience. See the APIs for lazy and Suspense for their exact loading contract.
Keep large collections cheap
A list with thousands of rich rows can overwhelm rendering even when each row is well written. Reduce the amount of work before trying to micro-optimize an individual component:
- paginate or incrementally load data when that matches the product;
- window very large scrolling collections so only visible rows mount;
- keep row components focused and avoid expensive layout reads;
- use stable IDs for
keyvalues.
type Result = {
id: string
title: string
}
export function Results({ results }: { results: Result[] }) {
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
)
}An array index is not a stable identity when items can move, appear, or disappear. Incorrect keys can cause extra work and, more importantly, preserve the wrong component state. React’s list key guidance treats keys as part of correctness, not only performance.
Reduce network, asset, and main-thread pressure
React rendering is only one part of page speed. A perfectly memoized tree still feels slow when it downloads oversized images, blocks on sequential requests, parses a large dependency, or triggers expensive layout.
Check these system-level costs:
- start independent data requests together instead of creating waterfalls;
- send the smallest data shape the screen needs;
- optimize image dimensions and formats;
- load fonts deliberately and avoid unnecessary weights;
- remove unused client dependencies;
- move expensive non-visual work away from urgent interactions;
- prefer server-rendered or statically generated content when interactivity is unnecessary.
Verify the result
Repeat the same scenario on the same device and data set. Compare the Profiler commit duration and render count, then check browser traces and user-facing metrics. Run interaction and visual tests because a faster implementation that returns stale data or breaks focus behavior is a regression.
A useful performance change records:
- the original user-visible symptom;
- the trace or profile that explains it;
- the code change and why it should help;
- before-and-after evidence;
- the tests protecting behavior.
Practical performance checklist
- Reproduce one slow user interaction consistently.
- Profile before adding memoization.
- Keep transient state local.
- Derive values during render instead of chaining Effects.
- Keep rendering pure.
- Let React Compiler handle routine memoization when enabled.
- Split substantial code at real interaction boundaries.
- Paginate or window genuinely large collections.
- Use stable data IDs as keys.
- Recheck network, JavaScript, layout, and image costs.
- Verify production behavior and protect it with tests.
Performance is a loop, not a one-time cleanup: measure, remove the largest unnecessary cost, verify the experience, and stop when the evidence says the result is good enough.
