iTranslated by AI
Why Doesn't React Memoize Components by Default?
Introduction
My name is taro, and I usually develop B2B SaaS at a startup.
This time, while thinking about React memoization, I came up with the question:
"Why doesn't React memoize components by default?"
To resolve this, I summarized what I researched and thought about! I've also included a review of prerequisite knowledge, such as the timing of renders and why memoization prevents re-renders, so those who aren't familiar with memoization can use this for study. Please give it a read!
Why did I have this question?
First of all, here is the background of why I had the question in the title. The thought process leading up to it was like this:
"The screen is heavy because of unnecessary re-renders, so I want to optimize it..."
→ "Let's memoize the component using React.memo()!"
→ "Thinking about whether to memoize for each component is a hassle..."
→ "It's a pain to think about it every time, so why not just memoize everything?"
→ "Besides, I don't think there are any components where memoization would be a problem..."
→ "Then why doesn't React memoize by default?"
In short, it's:
"Using React.memo() improves performance, and there don't seem to be any components where memoization causes issues, so why isn't it done by default?"
So, I researched various things about React.memo(). Before getting to the main point, I'd like to align our prerequisite knowledge by briefly reviewing:
- When are React components re-rendered?
- Why can
React.memo()suppress re-renders?
(If you don't need the review, please jump to Returning to the original question once again!)
A little review to align premises
First, let's review the timing of when a React component is re-rendered.
When are React components re-rendered?
Normally, the timing for a React component to re-render is:
- When the parent component is re-rendered
- When the state is updated
- Execution of
useStatesetter - Execution of
useReducer'sdispatch() - Execution of Class Component's
this.setState()
- Execution of
When you search for [React render when], some articles also mention when props are updated, but this only applies to memoized components, which I'll mention later.
React re-renders all child components unconditionally when 1. the parent component is re-rendered, so it doesn't check whether props have been updated.
Side note: Does calling setState() with the same value cause a re-render?
A bit off-topic, but if you call setState() with the same value as the state before the change, will it trigger a re-render?
The answer is:
- state is primitive → No re-render
- state is object → No re-render if it's the same object.
Actually, when updating state, React internally performs a shallow comparison between the state before and after the change. If they are the same, it doesn't re-render.
For example, when you execute a useState setter, a function called dispatchSetState() (similar to useReducer's dispatch()) is executed, and within it:
if (objectIs(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the Componentre-renders for a different reason and by that
// time the reducer has changed.
return;
}
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
var objectIs = typeof Object.is === "function" ? Object.is : is;
A shallow comparison is performed like this. If the values are identical, the process returns and stops, so no re-render occurs. (As noted in the code comment: without scheduling React to re-render!)
Therefore, you can confirm that if the state is primitive and you pass the same value, or if it is an object and you pass the exact same object to the setter as the updated state, no re-render occurs.
Conversely, be aware that even if the content of an object is exactly the same, it will be recognized as a state change and trigger a re-render if it is a different object.
const hoge = {bar: 'bar', foo: 'foo'}
const fuga = {bar: 'bar', foo: 'foo'}
hoge === hoge // -> true
hoge === fuga // -> false
hoge === {bar: 'bar', foo: 'foo'} // -> false
hoge === {...hoge} // -> false
Timing of React component re-renders
Once again, there are two timings for a React component to re-render:
- When the parent component re-renders
- Re-renders unconditionally without checking for changes in props.
- When the state is updated
- Only when there is a difference after shallow comparing the state before and after the change.
Now that we've covered the re-render timings, let's review why React.memo() can suppress re-renders.
Why can React.memo() suppress re-renders?
To begin with, React.memo() is a function that takes a component as an argument and returns a component (it wraps the component). (This is also called a Higher-Order Component.)
// By simply wrapping with React.memo, the component is memoized
const MemoComponent = React.memo((props) => {
// Processing using props
return <Hoge />
})
A component wrapped in React.memo() will not re-render if its parent component re-renders and there are no changes to the props.
Also, by default, the props are compared using shallow comparison, but you can customize the comparison method by passing a custom comparison function as the second argument to React.memo().
const equalFunction = (prevProps, nextProps) => {
// Custom comparison logic
}
const MemoComponent = React.memo((props) => {
// Processing using props
return <Hoge />
}, equalFunction)
However, based on the information I've gathered, I get the impression that customizing the comparison function is not very common. Therefore, in the following sections, when I refer to memoizing a component, I mean memoization using the default shallow comparison without a custom comparison function.
By the way, components that do not have props can also be memoized with React.memo(). Since there are no props to begin with, memoizing them will prevent them from re-rendering even if the parent component re-renders.
The reason why React.memo() suppresses re-renders
So, the reason why React.memo() suppresses re-renders is:
"Because a component wrapped in React.memo() does not re-render if there are no changes in props, even when the parent component re-renders."
Review Summary
Thanks for following the review so far!
Finally, let's briefly summarize what we've reviewed.
Timing of React component re-renders
- When the parent component re-renders
- Re-renders unconditionally without checking for changes in props.
- When the state is updated
- Only when there is a difference after shallow comparing the state before and after the change.
The reason why React.memo() suppresses re-renders
React.memo() suppresses re-renders because a component wrapped in it performs a shallow comparison of the props before and after the change even when the parent component re-renders, and does not re-render if there are no changes.
Now, let's return to the question in the title once again.
Returning to the original question once again
Again, the question I had this time is:
"Why doesn't React memoize components by default?"
However, there might naturally be components with requirements that would make default memoization problematic.
Therefore, let's first consider the decision criteria for whether to memoize a component, and then think about whether there are components that should not be memoized.
Decision criteria for memoizing a component
A memoized component only re-renders when there is a change in its props, even if its parent component re-renders.
In other words, the decision criteria for memoizing a component can be thought of as:
- Components that should re-render only when props change as a result of the parent component re-rendering
- ⭕ Should be memoized
- Components that should re-render even when props don't change as a result of the parent component re-rendering
- ❌ Should not be memoized
Now, what kind of component would be "a component that should re-render even when props don't change"?
Do components exist that should re-render even when props haven't changed?
Thinking simply, if only the parent component re-renders and the child component does not, the child component alone will become outdated. For example, a component required to maintain the freshness of displayed data might be a candidate.
However, in that case, the responsibility of maintaining freshness lies with the child component.
Therefore, I believe that delegating the timing of freshness to the parent component's re-render is not the correct way to handle responsibility.
I considered various situations, but assuming correct separation of responsibilities, I couldn't think of any such component.
Therefore, I thought that "components that should re-render even when props haven't changed" either do not exist or are components for very niche purposes. (Please note that this is my personal interpretation.)
Should all or most components be memoized?
In other words:
- Components that should re-render only when props change as a result of the parent component re-rendering
- ⭕ Should be memoized
- Components that should re-render even when props don't change as a result of the parent component re-rendering
- ❌ Should not be memoized
- Purpose doesn't exist, or the component is for a very niche use case.
Given this, it started to feel like all or most components could be memoized.
So why doesn't React memoize components by default?
Why doesn't React memoize components by default?
While researching, I found a tweet from Dan Abramov, the creator of Redux and a current React developer, where he mentions component memoization.
Dan's view on component memoization
Here is that tweet.
Apparently, the overhead of shallow comparison caused by memoization is proportional to the number of props. As a result, if a re-render occurs anyway, this shallow comparison process becomes a waste, and since many components receive different props, it's not always certain that comparing is faster.
Let's also look at the React source code
Since we're at it, let's see how the update process for memoized components is handled in the React source code.
First, here is the function that updates memoized components:
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
// omitted
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}
With compare = compare !== null ? compare : shallowEqual;, it uses the custom comparison function passed as the second argument to React.memo() if it exists; otherwise, it sets shallowEqual as the function to compare props. Then, compare(prevProps, nextProps) compares the props before and after the change.
Next, here is the shallow comparison function:
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
You can see that it iterates through the props using a for loop with for (let i = 0; i < keysA.length; i++) and performs a shallow comparison using is(objA[currentKey], objB[currentKey]).
It is indeed proportional to the number of props.
By the way, is() is the same function used for the shallow comparison of state before and after changes in the useState setter mentioned earlier.
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
Conclusion
The conclusion to "Why doesn't React memoize components by default?" is exactly what Dan stated in his tweet:
- The overhead of shallow comparison from memoization is proportional to the number of props.
- If a re-render ends up happening, this shallow comparison process is wasted.
- Since many components receive different props, it's not always certain that comparing is faster (shallow comparison is often wasted).
Therefore, it seems React does not memoize components by default. Since memoization intended to save performance can actually degrade it, the decision of whether to memoize is left to the developer's discretion.
Extra
Finally, just a little extra.
- How much does the overhead of shallow comparison affect re-render speed?
- If Record & Tuple are introduced, components might be memoized by default...?
How much does the overhead of shallow comparison affect re-render speed?
Let's use CodeSandbox to create a component with a massive number of props and see how much difference memoization makes in the time it takes for a re-render.
As a method, we will prepare two components with a large number of props—one memoized and one not—re-render the parent component to change the props, and compare the time required for both components to re-render.
To measure the time required for re-render, use the Profiler in the Google Chrome extension React Developer Tools. When using the CodeSandbox above, you can use the Profiler by opening a new tab via Open preview in new window.
Here is the time required for re-render when there are 10,000 props.
(The 2nd row is the memoized component, and the 3rd row is the non-memoized component)



While 10,000 is quite an extreme example, the memoized version takes an overwhelming 10 to 30 times longer for the re-render.
When the number of props was reduced to 1,000, the difference became about 3 to 5 times.



Well, since both the components and props are very simple, these aren't very practical numbers, but we could certainly confirm that the memoized component takes longer to re-render and that the time required increases in proportion to the number of props.
If Record & Tuple are introduced, components might be memoized by default...?
Based on a comment from KuroPanda, if Record & Tuple is introduced, it might become possible to compare props without the overhead being proportional to the number of props. Therefore, the day may come when components are memoized by default. (This is just a possibility, so please take it as a reference!)
Record and Tuple are respectively:
- Record: An object-like data structure
#{ x: 1, y: 2 } - Tuple: An array-like data structure
#[1, 2, 3, 4]
In these cases, the === comparison becomes a deep comparison instead of a shallow comparison. This eliminates the need to loop through each property of the props using a for loop, making it possible to compare props regardless of their count.
// Object
const hoge = {a: 1, b: 2}
hoge === {a: 1, b: 2} // -> false
// Record
const fuga = #{a: 1, b: 2}
fuga === #{a: 1, b: 2} // -> true
Summary
In this article, I summarized my thoughts and research to resolve the question: "Why doesn't React memoize components by default?"
The answer to the question is as stated in the Conclusion above: because memoization can potentially degrade performance instead.
Therefore, I realized it is better to carefully judge whether to memoize for each component. Since my knowledge of memoization is still insufficient to establish these decision criteria, I plan to study more and write an article once I've summarized it. I would be happy if you read it then!
I look forward to your feedback, suggestions, and questions!
Resources I used
Discussion
関数コンポネートにuseSelectorみたいなグルバール的なフックを使っている場合は、React.memoしたらいけないですね。
Object.Isで弾かれましたら、したのscheduleUpdateOnFiberを呼ばなく、再レンダリング走りません
deepな比較になるなら、表面上for文が書かれないだけで実際にはshallowよりも多くの計算が必要になってしまうのではないでしょうか?
immutableであることを活かしてJavaScriptエンジンが計算量を落とすことは理屈上可能だと思いますが、現状のプロポーザルでは計算量が線形未満になることは保証しておらず、実装上は計算量が線形になりそうであることを示唆しています。