Blog

Why Your React App Re-Renders and How to Stop It

React rendering is cheap until it isn't, so understanding when components re-render and how to prevent wasted work keeps complex interfaces responsive.

June 14, 20266 min readMuhammad Shehzaib
PERFORMANCEREACTRENDERING

React's rendering model feels deceptively simple: state changes, the component re-runs, and the screen updates. In practice, a single state update can cascade through dozens of components, and most of that work is invisible until your app starts feeling sluggish. The key insight is that a re-render is not the same as a DOM update. React re-runs your component function to build a new virtual tree, then diffs it against the old one and touches the DOM only where things actually changed. Re-running the function is usually fast, but allocating objects, running expensive calculations, and rebuilding large subtrees on every keystroke adds up. This article walks through what triggers renders, how to measure them, and the specific tools React gives you to skip work you do not need to repeat.

What Triggers a Render

A React component re-renders for exactly three reasons: its own state changed via a setter, its parent re-rendered, or a context value it consumes changed. That second reason surprises people. When a parent re-renders, every child re-renders by default, regardless of whether the child's props actually changed. This is why a state update near the top of your tree can ripple all the way down. Props changing does not by itself cause a render; rather, the parent rendering is what passes new props down. Understanding this ownership chain matters because it tells you where to intervene. If a deeply nested component is expensive, you cannot fix it by optimizing the component alone. You have to break the render propagation from above, either by memoizing the child or by restructuring where state lives so fewer ancestors re-run.

Measuring Before Optimizing

Never guess at performance problems. The React DevTools Profiler records every render in a session and shows you which components rendered, how long each took, and why each one ran. Turn on the setting that highlights why a component rendered, and you will often discover the culprit is a parent passing a freshly created object or callback on every render. The browser's own performance panel complements this by showing whether time is spent in your JavaScript, in layout, or in paint. A common mistake is reaching for memoization everywhere out of fear, which adds complexity and its own overhead. Measure first, find the components that render frequently and take real time, and optimize only those. Most components in a typical app render fast enough that wrapping them in memo would be pure ceremony with no benefit.

Memoizing Components With memo

React.memo wraps a component so it skips re-rendering when its props are shallowly equal to the previous render. It is the tool you reach for when a parent re-renders often but a particular child's props rarely change. The catch is shallow equality: memo compares props by reference, so if you pass an object or array literal as a prop, a new reference is created on every parent render and memo never helps. This is why memo so often appears alongside useMemo and useCallback, which stabilize those references. Treat memo as a targeted fix, not a default wrapper. A good candidate is a heavy list row, a chart, or a component with a large subtree whose inputs are stable. Wrapping tiny leaf components that already render in microseconds just adds a comparison cost for no measurable gain.

useMemo for Expensive Values

useMemo caches the result of a calculation between renders, recomputing only when one of its dependencies changes. Its legitimate use is twofold. First, when you genuinely have an expensive computation, like filtering and sorting thousands of rows, useMemo prevents redoing that work on every keystroke in an unrelated input. Second, it stabilizes object or array references you pass to memoized children or to dependency arrays, preventing them from triggering downstream renders. The trap is overusing it for cheap calculations, where the bookkeeping of storing and comparing dependencies costs more than just recomputing. Be honest about what counts as expensive: building a small array of five items is not. Also remember that useMemo is a performance hint, not a guarantee; React may discard cached values, so never rely on it for correctness or side effects.

useCallback and Stable Functions

useCallback memoizes a function definition so it keeps the same reference across renders until its dependencies change. Functions are recreated on every render in JavaScript, so an inline handler passed to a memoized child defeats that child's memo because the prop reference changes each time. useCallback fixes this by giving the handler a stable identity. It is also essential when a function appears in another hook's dependency array, such as a useEffect that should not re-run just because the callback was recreated. The mistake people make is wrapping every handler in useCallback reflexively, even handlers passed to plain DOM elements where reference stability does not matter at all. Like the other memo hooks, useCallback earns its keep only when paired with a memoized consumer or a dependency array that actually cares about reference equality.

Keys and List Reconciliation

When React diffs a list, it uses the key prop to match elements between renders. With stable, unique keys, React can tell that an item moved rather than assuming it was destroyed and a new one created, which preserves component state and DOM nodes. The classic anti-pattern is using the array index as a key. When the list reorders, gets items inserted, or has items removed, indices shift and React matches the wrong elements, causing input values to jump to the wrong rows and state to attach to the wrong items. Always key by a stable identifier from your data, like a database id. Keys also unlock an intentional technique: changing a key forces React to remount a component, discarding its state. This is the cleanest way to reset a form or a subtree when some identity changes, such as navigating to a different record.

Lifting Versus Colocating State

Where you put state directly determines how far renders propagate. State lifted to a high ancestor causes everything below it to re-render whenever it changes, even branches that do not use the value. The fix is often structural rather than memoization: push the state down to the smallest component that needs it, so updates stay local. When a fast-changing value like the text of a controlled input lives high in the tree, every keystroke re-renders the world. Colocating that input state in its own small component contains the damage. The mirror technique is lifting content up as children. A component can re-render while its children prop stays the same reference, and React skips re-rendering those children. Passing expensive subtrees as children to a frequently rendering wrapper is a clean, memo-free way to avoid wasted work.

Concurrent Rendering Tools

Modern React adds rendering primitives that change the timing of work rather than the amount. useTransition lets you mark a state update as non-urgent, so React can keep the interface responsive to typing while a heavy update, like filtering a large list, renders in the background and can be interrupted. useDeferredValue achieves a similar effect by letting a value lag behind, so an input stays snappy while expensive consumers catch up. These are not replacements for memoization; they manage priority, not cost, and you still want to avoid genuinely wasteful renders. For very large lists, the more impactful technique is virtualization, rendering only the rows currently visible in the viewport. Reach for transitions when an update is correct but janky, and for windowing when the sheer number of DOM nodes is the bottleneck rather than render frequency.