Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

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

  1. Choose one interaction. Define what feels slow and how to reproduce it.
  2. Record a baseline. Use React Developer Tools Profiler, the <Profiler> API, browser loading data, or interaction-latency measurements appropriate to the symptom.
  3. Identify the largest avoidable source of work. Check Effects, state ownership, unstable props, repeated calculations, and bundle boundaries.
  4. Make one targeted change. Remove unnecessary work before adding a cache; otherwise choose useMemo, useCallback, memo, lazy, or useDeferredValue according to the problem type.
  5. 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.
  6. 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.