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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Most slow React screens are not fixed by wrapping components in memo. The reliable order is to find the interaction that feels slow, measure which part of the tree does the work, remove updates that should not happen at all, and only then apply the smallest technique that fits what remains. This guide follows that order and explains what React’s official documentation says about each tool, including where each one stops helping.

Start with one interaction that feels slow

Pick a specific user action: typing in a search box, switching a tab, or opening a filter panel. Write down what should happen and what actually happens. Optimizing “the whole app” usually leads to memoizing components that were never part of the problem. A single interaction gives you a measurable target and a way to tell whether a change helped.

Check whether React Compiler already handles memoization

React Compiler can automatically memoize values, functions, and components. In a project where it is enabled in the build setup, many of the manual techniques below may be unnecessary, and adding them by habit can make code harder to read without a clear gain. Confirm the compiler’s status in your own build configuration before deciding that manual memoization is needed. If it is not enabled, the patterns in the rest of this article apply directly.

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

Measure the component tree before changing it

React’s Profiler component wraps a section of the tree and calls an onRender callback whenever a component inside that section commits an update. Use it to find out which subtree is expensive, not to guess.

  1. Open your app in development, then open the browser’s React Developer Tools and select the Profiler tab.
  2. Start a recording, perform the slow interaction once, and stop the recording.
  3. Inspect the commits that took the longest. Identify the component that is re-rendering and the parents that caused it.
  4. If the ranking is not specific enough, wrap the suspected subtree in a Profiler to get timing for that section alone:
import { Profiler } from 'react';

function onRender(id, phase, actualDuration, baseDuration) {
  console.log(id, phase, { actualDuration, baseDuration });
}

<Profiler id="ResultsList" onRender={onRender}>
  <ResultsList results={results} />
</Profiler>

The two timing fields answer different questions:

Field What it measures How to read it
actualDuration Time spent rendering the update that was just committed High values mean that update is costly as it stands
baseDuration Estimated render time for the subtree without memoization A large gap between this and actualDuration suggests existing memoization is already helping; similar values suggest the subtree renders fully each time
id and phase Which profiled section committed, and whether it was a mount or an update Separates first render cost from repeated update cost

Profile something closer to production

Profiling adds overhead, and development builds behave differently from production builds. React’s documentation for useMemo notes that development measurements are not always representative: Strict Mode can invoke render logic more than once in development. Test a production build, and use CPU throttling in your browser’s performance tools to approximate slower devices. The Profiler is disabled by default in production builds; React documents a separate profiling-enabled production build for cases where you need production timings. Use that build only when you need it, and compare results from the same environment before and after each change.

Remove update work that should not happen

Before adding any memoization, check whether the component is rendering because of avoidable updates. The most common cause is a chain of state updates driven by Effects. React’s useMemo documentation puts it directly: “Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.”

Keep transient state close to where it is used

State that only one small component needs should live in that component. When state sits high in the tree, every change re-renders everything beneath it, including components that do not read the value. Moving the state down is often a larger improvement than any memoization step, and it costs no added code.

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

Derive values during render instead of syncing them with Effects

A value that can be computed from props or state does not need its own state variable and Effect. The Effect version adds an extra render pass; the derived version does not.

// Avoid: an Effect copies derived data into state, causing an extra render
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// Prefer: compute it during render
const fullName = firstName + ' ' + lastName;

Simplify Effect dependencies before reaching for memoization

When an object or function appears in an Effect’s dependency list and changes every render, the first fix is to move it inside the Effect or outside the component if it does not depend on props or state. Wrapping it in useMemo or useCallback just to keep the dependency stable adds a second mechanism that must stay correct. Do that only when moving the code is not possible.

Use useMemo for a calculation that is measurably slow

useMemo caches the result of a calculation between renders. On a later render, React reuses the cached value if every dependency is equal under Object.is. React’s documentation states that it “will not throw away the cached value unless there is a specific reason to do that,” which means the cache holds only as long as its inputs stay the same and React has no reason to discard it.

const visibleItems = useMemo(
  () => filterItems(items, query),
  [items, query]
);

Use it in two situations:

  • The calculation is noticeably expensive, and its inputs often stay the same across renders (for example, a re-render triggered by an unrelated state change).
  • You need a stable value to pass to a child wrapped in memo, so the child can skip rendering.

Three constraints matter. The calculation must be pure. Dependencies must be complete; a missing dependency returns stale results. And useMemo does not make the first render faster, because nothing has been cached yet. If the calculation is cheap, or its inputs change on every keystroke, the cache rarely helps and only adds code.

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.

Use memo for a child that measurement shows is expensive

memo(Component) lets React skip re-rendering a component when its props have not changed. By default, each prop is compared with Object.is. This is where most disappointing results come from. A new object, array, or function created during the parent’s render has a new identity every time, so the comparison fails and the child renders anyway.

const ResultRow = memo(function ResultRow({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.label}</li>;
});

Several limits apply. The component still re-renders when its own state changes or when a context it reads changes. A custom comparison function can avoid a re-render, but it runs on every render and can cost more than the render it prevents. React’s documentation for memo states that “memoization is a performance optimization, not a guarantee,” so treat a skipped render as a bonus that you verify in the Profiler, not something to rely on.

Keep input responsive with useTransition and useDeferredValue

Sometimes the work cannot be removed. A user types into a search box, and filtering a large list makes each keystroke slow. Both useTransition and useDeferredValue let React separate urgent updates from non-urgent ones, so the input can update first.

  • Use useTransition when you control the state update that triggers the expensive render. Wrap that update in startTransition.
  • Use useDeferredValue when the expensive part receives a value you do not set at the point of update, such as a prop or a value from a parent.
function SearchPage({ items }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const results = useMemo(
    () => filterItems(items, deferredQuery),
    [items, deferredQuery]
  );

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ResultsList results={results} />
    >
  );
}

The tradeoff is visible to users. While urgent input is current, the deferred section can briefly show results for an older value. These hooks reprioritize rendering; they do not make the filter itself cheaper. If filtering is still slow after the input stays responsive, the calculation or the list rendering needs its own work.

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

Defer code you do not need at first paint with lazy and Suspense

lazy delays loading a component’s code until the component is first rendered. Suspense shows a fallback while its children are loading. Together they reduce the initial bundle for features that most visitors do not open at once, such as a chart in a rarely visited dashboard tab.

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  return (
    <Suspense fallback={<p>Loading chart…</p>}>
      <Chart />
    </Suspense>
  );
}

Place the boundary where the fallback makes sense to the user. A single boundary around a large page can hide much of the page behind one spinner, while several small boundaries can show content in pieces.

React 19 changes how suspended siblings are committed

The React 19 upgrade guide, published 2024-04-25, describes a change in how React handles suspense. When a component suspends, React can commit the nearest fallback without waiting for the entire sibling tree, and then schedule the suspended siblings to pre-warm their lazy requests. This is version-specific behavior: if your project is on an earlier React version, expect the older commit timing instead.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Compare the techniques by what they change

The table below puts the main patterns side by side. The key question is whether a technique removes work or only reprioritizes it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Situation Candidate pattern What it changes What must hold for it to help
A pure calculation is measurably slow and its inputs are stable useMemo Reuses a calculated value across updates Dependencies are complete and stable; the first render is unaffected
A child is costly and its props often stay the same memo with stable props Can skip some child renders Props keep their identity; the component’s own state and context still trigger renders
Typing or another urgent action competes with expensive UI work useTransition or useDeferredValue Prioritizes urgent rendering over non-urgent rendering The deferred section can briefly show older results
A rarely needed component adds to initial code size lazy with Suspense Delays code loading and shows a fallback The boundary and fallback suit the user’s flow; React 19 commit timing applies in React 19 projects
Repeated renders come from state updated inside Effects Simplify state and Effects Removes avoidable update chains The value can be derived during render instead of stored in state

Decision order for a slow screen

  1. Reproduce the slow interaction and record it in the Profiler, using a production build with CPU throttling where possible.
  2. Confirm whether React Compiler is enabled in your build.
  3. Remove avoidable updates: move state down, derive values during render, and simplify Effect chains.
  4. Classify what remains. A slow calculation points to useMemo. A costly child with stable props points to memo. Urgent input competing with expensive output points to useTransition or useDeferredValue. Large code that is not needed at first paint points to lazy with Suspense.
  5. Apply one change at a time and re-measure the same interaction.

If the numbers do not improve

  • Check that the expensive commit is the one you targeted. A sibling subtree may dominate the timing.
  • Look for inline objects, arrays, or functions passed to memoized children. They silently defeat memo.
  • Check the dependency list of each useMemo. A missing dependency produces stale output, and an unnecessary one causes recalculation.
  • Remove a memoization that measured no gain. It adds code and comparison cost without benefit.

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.