React Rendering Performance: Making Updates Cheap

This topic is part of Framework Performance. React's model is simple: when state changes, the component that owns the state re-renders, and by default so do all of its descendants. React then compares the new element tree with the previous one and applies the minimal DOM changes. This is fast for small trees and occasional updates. It becomes slow when a state change near the top of the tree re-renders hundreds or thousands of components, when components do expensive work during render, or when updates happen on every keystroke.

These costs land directly on Interaction to Next Paint. A click handler that sets state triggers a render and commit before the browser can paint; if that render takes 150ms on a mid-range phone, INP for that interaction is at least 150ms. Hydration — React rendering the whole tree once on the client to attach to server HTML — adds to Total Blocking Time and to the INP of interactions that happen during load.

React provides tools at several levels: profiling (React DevTools Profiler), memoization (React.memo, useMemo, useCallback), automatic memoization (the React Compiler), state design (colocating state, splitting context), and concurrent rendering (useTransition, useDeferredValue) that keeps urgent updates responsive while expensive ones render in the background.

What happens on a React state update Steps from a state update through rendering, reconciliation and commit to the browser painting. What happens on a React state update setState in event handler Render component + descendants Reconcile diff element trees Commit DOM changes + effects Browser style, layout, paint

The Metric Degradation This Topic Addresses

Slow React rendering shows up as poor INP, particularly on interactions that update shared state (filters, selections, form inputs, toggles), and as long tasks during hydration that delay early interactions. In INP attribution, the time appears as processing duration (event handler plus synchronous render) and presentation delay (commit and the browser's rendering work). With the Long Animation Frames API, script attribution points at React's internals and your components.

Typical symptoms: typing in an input lags behind keystrokes; selecting a row in a table takes hundreds of milliseconds; opening a menu re-renders the whole page; hydration produces a single long task of 300ms or more on mobile.

Prerequisites

  • React DevTools with the Profiler, and a development or profiling build for detailed component timings.
  • Field INP data with interaction targets, to know which interactions to fix.
  • A Performance panel trace of the slow interactions with CPU throttling.

1. Environment Setup: Profile the Slow Interaction

Open React DevTools → Profiler, enable "Record why each component rendered while profiling" in settings, start recording, perform the interaction, and stop. The flame graph shows each commit; components are coloured by render time, and grey components did not render. The ranked view lists the most expensive components.

2. Capture a Baseline

Record the commit duration for the interaction and the number of components that rendered. In the Performance panel (with throttling), record the total interaction time. In the field, note INP for the interaction target.

3. Isolate Wasted and Expensive Renders

Wasted renders are components that re-rendered but produced the same output — usually because a parent re-rendered or a context value changed identity. Expensive renders are components whose render function itself is slow: large lists, heavy computations, complex charts. "Why did this render?" in the Profiler distinguishes props changes, state changes, context changes and parent renders.

javascript
// Common cause: a new object/array/function identity on every parent render.
function Page({ products }) {
  const [query, setQuery] = useState('');
  const options = { currency: 'EUR' };                     // new object every render
  return <ProductGrid products={products} options={options} onSelect={(id) => track(id)} />;
}
// trade-off: inline objects and callbacks are fine for cheap children; they only
// matter when the child is memoized or expensive, where they defeat memoization.

4. Apply: Render Less, Render Cheaper, Render Later

Render less by colocating state (move it down to where it is used), splitting context, and memoizing expensive subtrees (or adopting the React Compiler). Render cheaper by virtualizing long lists and moving expensive calculations out of render. Render later by marking non-urgent updates with useTransition or useDeferredValue.

javascript
// Colocate state: the input's state lives in the input component, not the page.
function SearchBox({ onSearch }) {
  const [value, setValue] = useState('');
  return <input value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && onSearch(value)} />;
}
// trade-off: the page only learns the query on Enter; for live search, combine
// with useDeferredValue so results update without blocking typing.

Commit time for a filter change on a 500-row table Bar chart of React commit duration for a filter change before and after successive rendering optimizations. Commit time for a filter change on a 500-row table Baseline (whole page re-renders) 240ms State colocated 150ms + memoized rows 70ms + virtualized table 18ms budget

Deconstructing React's Rendering Rules

A component re-renders when its state changes, when its parent re-renders (unless it is memoized and its props are shallow-equal), or when a context it consumes changes value. Memoization with React.memo skips re-rendering when props are equal by reference; it is defeated by new object, array or function props created on every render — hence useMemo and useCallback to stabilise them. Context consumers re-render whenever the context value changes identity, regardless of which part of the value they use.

The React Compiler applies memoization automatically at build time: it analyses components and hooks, and caches JSX, computed values and callbacks so that re-renders skip work when inputs have not changed. It removes most of the need for manual useMemo, useCallback and memo, provided code follows the Rules of React (pure render, no mutation of props or state).

Concurrent rendering changes when work happens rather than how much. Updates wrapped in startTransition are interruptible: React renders them in the background, yields to the browser between chunks, and abandons stale renders when newer input arrives. Urgent updates — the input value itself — render immediately.

Advanced Diagnostics and Edge Cases

Hydration cost. Large client component trees hydrate in one go unless split by Suspense boundaries. Server Components reduce what hydrates at all.

State management libraries. Global stores that notify all subscribers on every change can cause broad re-renders; use selectors that subscribe to slices.

Effects that set state. useEffect that sets state after render causes a second render; derive values during render instead where possible.

Development mode. React's development build and Strict Mode double-invoke renders; always measure with production builds.

Lists without keys or with index keys. Cause unnecessary DOM work and state bugs when items reorder.

Validation and Budgeting

After changes, re-profile: fewer components should render per interaction, commit durations should drop, and the Performance trace should show shorter tasks. In the field, INP for the target interactions should improve. Budget commit duration for key interactions (for example under 50ms on a 4x throttled profile) in lab tests.

CheckTarget
Commit duration for key interactions (4x CPU)< 50 ms
Components rendered for a local updateOnly the affected subtree
Hydration long task (mobile profile)< 200 ms per task
INP p75 for key interactions< 200 ms

Worked Example: A CRM Contact List

A CRM's contact list page held filters, selection and the contact list in a single page component with a context providing the whole state object to all children. Typing in the search box re-rendered the page, 800 contact rows and the sidebar on every keystroke: 180ms per keystroke on a mid-range laptop with throttling, and INP p75 of 340ms. The team moved the search input's state into the input component, passed the query to the list through useDeferredValue, split the context into a selection context and a filters context, memoized the row component, and virtualized the list. Keystrokes now committed in 4ms; the list updated in the background in about 30ms. INP p75 dropped to 120ms.

When Memoization Helps — and When It Does Not

Memoization has a cost: comparing props and storing results on every render. For cheap components, that cost can exceed the render it skips. It pays off for components that are expensive to render, render often with unchanged props, or sit at the top of large subtrees. A practical rule: measure first, memoize the few components that show up at the top of the Profiler's ranked view, and stabilise the props they receive. Or adopt the React Compiler and let it handle the common cases consistently.

Concurrent Features for Responsiveness

useTransition and useDeferredValue do not make rendering faster; they make it interruptible and lower priority, so the browser can paint urgent updates (and respond to input) while expensive rendering continues. They are ideal for search-as-you-type, filter changes on large lists, tab switches that render heavy content, and route transitions. They cannot help with a single synchronous computation that blocks the thread — that still needs to be made cheaper, chunked, or moved to a worker.

Keystroke handling with and without useDeferredValue Timeline showing a keystroke blocked by list rendering compared with an urgent input update and deferred list rendering. Keystroke handling with and without useDeferredValue Without render input + 800 rows paint With deferred value input paint list renders in background 0ms 50ms 100ms 150ms 200ms 250ms 300ms INP poor

Hydration and Server Components

Rendering performance starts before the first interaction. On a server-rendered React page, hydration renders every client component once in the browser, and that render runs while users may already be trying to tap and scroll. Two levers reduce it. Server Components (in frameworks that support them, such as the Next.js App Router) never hydrate: their output is HTML plus a serialised description, and their code is not sent to the client. Moving static parts of the page — content, layout, product descriptions — into server components and keeping client components small and leaf-level shrinks both the bundle and the hydration work. Suspense boundaries split hydration into independent chunks, and React prioritises hydrating the boundary a user interacts with first (selective hydration), so a tap on a button in one section does not wait for the whole page.

A useful audit is to list the client components on a key page and ask, for each, whether it needs state, effects or event handlers. Components that only render props often became client components by accident (a "use client" directive at the top of a shared module pulls everything it imports into the client bundle).

Expensive Work Outside Render

Not all slow interactions are rendering problems. Event handlers that process large data synchronously, effects that run expensive logic after commit, and third-party scripts that react to DOM changes all contribute to INP. The browser Performance panel shows the full picture: React's render and commit appear alongside your handler code, effects, and the browser's style, layout and paint. If the handler itself is slow, break the work into chunks with scheduler.yield() or move it to a Web Worker; if effects are slow, check whether the work can run during render (derived state) or be deferred. Fixing only React rendering when the handler is the bottleneck brings little improvement.

Testing Rendering Performance in CI

Rendering regressions are easy to introduce — a new context, a prop that changes identity, an unmemoized list — and hard to spot in code review. Add a few automated checks for key interactions: a Playwright or Puppeteer script that performs the interaction on a profiling build and records render counts or commit durations via the <Profiler> callback, failing if they exceed a budget. Even a simple assertion such as "typing one character in search renders fewer than 50 components" catches most cascades before they reach users.

A Rollout Plan

  1. Rank interactions by field INP and profile the worst in React DevTools.
  2. Colocate state and split context to shrink what re-renders.
  3. Memoize the expensive components the Profiler identifies, or adopt the React Compiler.
  4. Virtualize long lists and move heavy computation out of render.
  5. Wrap non-urgent updates in transitions or deferred values.
  6. Re-measure in the lab and field; add commit-time checks for key interactions.

Common Pitfalls

  • Memoizing everything by hand. Adds overhead and noise; target expensive components.
  • One context for all app state. Every consumer re-renders on any change.
  • State too high in the tree. Local UI state stored in page-level components.
  • Profiling development builds. Misleading numbers due to development checks.
  • Expecting transitions to fix slow computations. They defer work, not shrink it.

FAQ

What is a wasted render?

A render that produces the same output as before, usually because a parent re-rendered or a prop changed identity without changing value.

Should I wrap every component in React.memo?

No. Memoize components that are expensive or render frequently with unchanged props. For others, memoization adds overhead without benefit.

Does the React Compiler replace useMemo and useCallback?

For most cases, yes. It memoizes automatically at build time. Manual memoization may still be useful for specific cases the compiler cannot optimise.

How do I find what caused a re-render?

Enable "Record why each component rendered" in React DevTools Profiler settings; each component's details show whether props, state, context or a parent caused it.

Does useTransition make my app faster?

It makes urgent updates responsive by rendering non-urgent ones in the background and interruptibly. Total work is similar.

Why is hydration slow?

React renders the full client component tree once to attach to server HTML. Large trees, expensive components and third-party scripts running at the same time make it slow.

Do Server Components help INP?

Indirectly: less client code and less to hydrate means fewer long tasks during load. Interactions in client components still follow the same rendering rules.

Is React slower than other frameworks?

React’s default re-render model does more work per update than fine-grained reactive frameworks, but well-structured React apps — with colocated state, memoization or the compiler, and virtualization — easily meet INP targets. Architecture matters more than the framework.

Should I use a state management library for performance?

For shared state that changes often, a store with selectors limits re-renders to components that use the changed slice. For rarely changing values, context or props are fine.

Guides in This Topic