Speed up a React app by measuring a reproducible slow interaction first, then fixing the specific source of work. Use the React Developer Tools Profiler or the <Profiler> API to identify expensive commits; only then consider targeted memoization, code splitting, or deferred rendering. useMemo, useCallback, and memo can reduce repeated work, while lazy reduces initial code loading and useDeferredValue protects input responsiveness.
1. Measure the slow interaction before changing code
Start with a user action you can reproduce: typing into a search box, opening a route, expanding a data-heavy panel, or switching a filter. Record that interaction with React Developer Tools Profiler. Look for components that render repeatedly, commits that take substantial time, and updates that affect more of the tree than the interaction requires.
For measurements in automated runs or production diagnostics, wrap the relevant tree in React’s <Profiler>. Its onRender callback runs whenever that tree commits, allowing you to record and compare rendering behavior after each change.
<Profiler id="Results" onRender={(...commitData) => {
performance.mark(`results-${commitData[1]}`);
}}>
<Results query={query} />
</Profiler>
Keep loading performance and rendering performance separate. Use browser loading data for JavaScript delivered at startup, Profiler commit data for React rendering, and interaction latency for how quickly an input responds. A change that improves one signal can leave the others unchanged.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →#1 Best Overall
2. Find the cause of unnecessary work
Before adding a Hook, inspect the update path that the Profiler exposed. Common causes include:
- An Effect sets state after every render or whenever a value that could have been calculated during rendering changes.
- State is lifted higher than necessary, so a small interaction re-renders unrelated siblings.
- An object, array, or function prop is recreated on every parent render and therefore appears changed to a child.
- An expensive filter, sort, parse, or transformation runs on every render even though its inputs rarely change.
- A large feature is included in the initial bundle even though most users reach it later.
Fix the source first: remove an unnecessary Effect, keep state near the components that use it, split unrelated subtrees, or move a calculation outside rendering when it does not depend on component state. Memoization is most useful after these structural issues are understood.
3. Use useMemo for a demonstrably expensive calculation
useMemo caches the result of a calculation between renders. It is appropriate when the calculation is noticeably slow, its dependencies are explicit and usually stable, or its stable result lets a memoized child skip work.
const visibleRows = useMemo(
() => sortAndFilterRows(rows, sort, query),
[rows, sort, query]
);
What the cache does—and does not do
- It can avoid repeating the calculation when every listed dependency is unchanged.
- It does not make the first render faster.
- React may discard a cached value in specific situations, so correctness must never depend on the cache.
- Most calculations are already fast; caching adds dependency bookkeeping and can reduce readability.
Keep the calculation pure and include every value it reads in the dependency list. Profile before and after the change. If the calculation is cheap or its dependencies change on nearly every render, the cache may cost more than it saves.
4. Use useCallback when a function reference matters
useCallback caches a function definition so React can return the same function reference while its dependencies remain unchanged. A new function is still created while the component renders; the Hook controls which reference is exposed to consumers.
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
Good reasons to add it
- A memoized child receives the callback and can skip rendering when the callback reference stays stable.
- The callback is a dependency of an Effect and changing its identity would retrigger that Effect.
- A custom Hook uses function identity to avoid unnecessary work.
Do not use useCallback to make code correct. Include all values captured by the function in its dependency list, and verify that stabilizing the reference actually removes meaningful work. If the receiving child is not memoized and no Hook depends on identity, the extra bookkeeping usually provides no benefit.
Rank #3
5. Apply memo only where prop equality can skip real work
Wrapping a component in memo lets React skip a child render when its props are unchanged. This is conditional optimization, not a promise: React may still render the component, and a single prop that is always new can defeat the comparison.
const Row = memo(function Row({ item, onSelect }) {
return <button onClick={() => onSelect(item.id)}>{item.name}</button>;
});
memo is most useful for a component that performs measurable work and receives stable primitive, object, or function props. Pair it with useMemo or useCallback only when those props would otherwise be recreated and profiling shows the child is a significant part of the interaction. Avoid blanket-wrapping every component; comparisons, dependency maintenance, and indirection also have a cost.
6. Defer feature code with lazy
lazy defers a component’s code until the component is rendered. That changes the loading boundary rather than making the component’s own render faster, so it is a natural fit for route-level screens and unusually heavy features that many users do not open immediately.
Rank #4
const Reports = lazy(() => import('./Reports'));
function App() {
return (
<Suspense fallback={<LoadingPanel />}>
<Reports />
</Suspense>
);
}
Place an appropriate loading UI around the deferred component and measure both startup loading and the first visit to the feature. Splitting too aggressively can create many small requests or make frequently used content wait, so choose boundaries that match navigation and user behavior.
7. Keep fast-changing inputs responsive with useDeferredValue
When an input changes rapidly but the view derived from it is expensive, useDeferredValue lets the parent render the new input quickly while a slower value catches up.
function SearchPage({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => search(items, deferredQuery),
[items, deferredQuery]
);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<Results items={results} />
</>
);
}
The expensive subtree must be structured so the deferred value actually reduces urgent work. Deferring a value does not eliminate the calculation; it changes when the slower render competes with the user’s immediate interaction. Profile typing latency and the deferred view together.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Best Value
8. Understand how React Compiler changes manual memoization
Current React references describe React Compiler as able to automatically memoize values, functions, and components. In a project using a compatible Compiler setup, that can reduce the need for manually adding useMemo, useCallback, and memo.
Compiler behavior depends on the React version and toolchain configuration. Confirm that it is enabled for the code being measured before removing existing optimizations, and continue profiling: automatic memoization does not fix an unnecessary Effect, an overly broad state update, a large initial bundle, or an inherently expensive calculation.
Quick Recap
Which technique matches the problem?
| Problem | Primary signal | Technique | Scope | Main trade-off |
|---|---|---|---|---|
| An expensive calculation repeats | Profiler shows calculation-heavy commits | useMemo |
One calculated value | Dependency bookkeeping and cache maintenance |
| A child re-renders despite equivalent inputs | Profiler shows avoidable child renders | memo, often with useCallback or useMemo |
One component boundary | Prop comparisons help only when props remain stable; memoization is not guaranteed |
| A callback’s identity retriggers work | Child or Effect responds to a new function reference | useCallback |
One function reference | The function is still created during rendering; dependencies must stay accurate |
| Initial JavaScript is too large | Browser loading data and startup timings | lazy with a loading boundary |
Route or heavy feature | Deferred features load later and may add a wait on first access |
| Typing or direct manipulation feels blocked | Interaction latency and Profiler commits | useDeferredValue |
Urgent parent update versus slower subtree | The deferred work still runs and must be isolated from urgent rendering |
| Manual memoization is widespread | Compiler-enabled build and profiling | React Compiler | Values, functions, and components across eligible code | Requires supported React and toolchain configuration |
A repeatable optimization workflow
- Choose one interaction. Define what feels slow and how to reproduce it.
- Record a baseline. Use React Developer Tools Profiler, the
<Profiler>API, browser loading data, or interaction-latency measurements appropriate to the symptom. - Identify the largest avoidable source of work. Check Effects, state ownership, unstable props, repeated calculations, and bundle boundaries.
- Make one targeted change. Remove unnecessary work before adding a cache; otherwise choose
useMemo,useCallback,memo,lazy, oruseDeferredValueaccording to the problem type. - Profile the same interaction again. Keep the change only if the relevant signal improves without introducing stale data, incorrect dependencies, or an unacceptable loading state.
- Recheck the production build. Development behavior, compiler settings, network delivery, and real data volume can differ from local development.
Practical checks before shipping
- Every memoized calculation and callback has a complete dependency list.
- No correctness rule relies on a cached value or stable function identity.
- Memoized children receive props whose identities are stable when their contents are unchanged.
- Deferred components have an intentional loading and error experience.
- Code splitting is applied to meaningful route or feature boundaries rather than indiscriminately.
- The same user interaction was measured before and after the optimization.
- If React Compiler is enabled, manual memoization is retained or removed based on measured behavior and the supported toolchain, not assumption.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

