Why React Doesn't Memoize Components by Default
React provides memoization mechanisms like React.memo and useMemo, but the framework’s default behavior follows a simple model: if a parent re-renders, its children execute too. So why not automatically memoize from the start to make things faster? This article explains the design philosophy and practical decision criteria, with code examples.
Terminology
Rendering is the phase where component functions execute to compute the virtual element tree; committing is the phase where the diff is applied to the real DOM. In general, commits tend to be more expensive. Memoization reuses the previous result for the same inputs to avoid unnecessary recomputation (renders). React.memo performs a shallow comparison of props and skips a child’s re-render if they are equal. However, it does not cover state updates, context changes, or updates inside hooks.
Why memoization is not the default
First, React prioritizes correctness and predictability. Automatic memoization can make update propagation opaque, increasing the chance that the UI does not update as expected. When effects (useEffect), layout synchronization, Suspense, or transitions are involved, it gets harder to reason about what should re-run and when. The default model (if a parent re-renders, its children execute too) is easier to learn and debug.
Second, memoization itself has overhead. You must shallow-compare every time and store the previous props. If you pay that comparison cost uniformly, even for lightweight components, things can get slower. The fewer renders you have, the more the comparison cost stands out. Adding memo is not always faster.
Third, it is hard to decide at runtime when to apply memoization automatically. It is difficult to safely infer which children are expensive and which props are referentially stable. In common patterns where you inline-create objects and functions, their references change on every render, defeating shallow comparison. Deep comparisons are even more expensive and counterproductive.
Additionally, alignment with concurrent rendering (interruptions, resumptions, prioritization) introduced in React 18 matters. Broad, automatic memoization can complicate where recomputation, interruption, and resumption happen, reducing the scheduler’s flexibility and potentially degrading perceived smoothness. From a DX perspective, if memoization were the default, developers might overuse useMemo and useCallback to stabilize references, shifting attention to dependency arrays and referential consistency, which hurts readability, maintainability, and robustness.
Common misconceptions
Re-rendering is not the same as DOM updates. Rendering is computation, and if there is no diff, the commit (DOM changes) hardly happens. Also, React.memo is not a silver bullet. It only skips when props are equal. It cannot prevent state changes, context updates, or the double invocation in development Strict Mode (which is intentional to surface issues).
When should you use React.memo?
Measure first. Use the Profiler in React DevTools to see which components render how often and for how long, and apply memoization when you know the bottleneck is in child renders. It is most effective when:
- You render many similar children in a list and each child’s render is expensive.
- The parent re-renders frequently but children usually do not change.
It is also important that props are referentially stable so shallow comparison is sufficient.
Designing to maximize the benefits
Stabilize the references of props you pass to children. Avoid creating new objects, arrays, and functions on every render; use useMemo and useCallback only where needed. Split frequently changing parts into smaller components, and extract heavy static parts into separate memoized components to limit render propagation. Passing primitives such as IDs and booleans also helps shallow comparison. But beware of over-optimization, which increases complexity and can introduce bugs. Always validate with measurements.
Code examples
First, a straightforward implementation. In many cases this is enough.
import React, { useState } from "react";
function UserItem({ user, onSelect }) {
console.log("render", user.id);
return (
<li onClick={() => onSelect(user.id)}>
{user.name} ({user.score})
</li>
);
}
export function UserList({ users }) {
const [filter, setFilter] = useState("");
const filtered = users.filter((u) => u.name.includes(filter));
return (
<>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="filter"
/>
<ul>
{filtered.map((u) => (
<UserItem
key={u.id}
user={u}
onSelect={(id) => {
/* do something */
}}
/>
))}
</ul>
</>
);
}If you find that child renders are expensive, stabilize references and then memoize the child.
import React, { memo, useCallback, useState } from "react";
const UserItem = memo(function UserItem({ user, onSelect }) {
// Assume heavy computation or layout measurement here
console.log("render", user.id);
return (
<li onClick={() => onSelect(user.id)}>
{user.name} ({user.score})
</li>
);
});
export function UserList({ users }) {
const [filter, setFilter] = useState("");
const filtered = users.filter((u) => u.name.includes(filter));
const handleSelect = useCallback((id) => {
/* do something */
}, []);
return (
<>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="filter"
/>
<ul>
{filtered.map((u) => (
<UserItem key={u.id} user={u} onSelect={handleSelect} />
))}
</ul>
</>
);
}Here is an anti-pattern that negates memoization. If you pass new references every time, the shallow comparison will always fail.
<UserItem
user={{ ...user }} // A new object each time
onSelect={(id) => {
/* inline */
}} // A new function each time
/>Context and concurrent rendering
React.memo does not block context updates. If a child reads a value with useContext, it will re-render when the context changes. In concurrent rendering, React can interrupt, resume, and prioritize renders. If the runtime applies broad automatic memoization, the scheduler loses flexibility in deciding where recomputation is needed, which makes optimal responsiveness harder to achieve. Keeping the default behavior simple helps preserve this freedom.
Summary
React does not memoize components by default to preserve correctness and predictability while avoiding the overhead of comparisons and cache maintenance, as well as potential downsides for concurrent rendering. Memoization is not magic. It is a local optimization that works when the right conditions are met. Start simple, measure with the Profiler, apply it carefully only where it matters, and re-measure to verify. This basic cycle is the reliable path to effective performance tuning.