Improving Performance with React.memo
February 18, 2020
As you write an application in React, preventing extra renders can provide a huge performance improvement.
To understand React.memo
we’ll look at the following:
- What is
React.memo
? - How does
React.memo
fit into the React update process? - An example of memoization.
- When to use
React.memo
and when it’s unnecessary.
What is Memoization?
Memoization is a fancy way of saying ‘store work that you’ve already done’.
Here’s a basic implementation:
const cache = {}
const memo = input => {
// if our cache already has the result stored,
// return the value without computing it again
if (cache[input] != null) {
return cache[input]
}
// if we don't have the result, compute it
// and store the result in the cache for future calls
const result = computeSlowThing(input)
cache[input] = result
return result
}
React supplies a higher order component called React.memo that is a version of memoization for functional components.
It allows you to tell React to skip rendering a component when it’s props don’t change. This can provide a performance benefit in certain scenarios.
It’s most basic usage looks like:
React.memo(MyComponent)
If you’re familiar with React.PureComponent for class components, memo is the equivalent for functional components.
What happens when a components updates?
When a component updates, React renders it and compares that render result with the previous render result. If that result is different, React updates the DOM.
While this comparison may be fast, what if your component is slow to render?
Memo allows us to tell React to only render the component again if the props have changed.
Using React.memo
Let’s take a look at an example without memo. The CurrentTrack
component below will display the song title, artist and how much of the song has played so far.
With each second that passes, currentTime
will update to reflect the new position in the song.
const CurrentTrack = ({
title,
artist,
albumArtwork,
duration,
currentTime,
}) => {
return (
<div>
<div>Track Title: {title}</div>
<div>Artist: {artist}</div>
<img src={albumArtwork} alt="album artwork" />
<div>
{currentTime} / {duration}
</div>
</div>
)
}
export default CurrentTrack
Since currentTime
updates every second, the props will change at least once per second.
Each time props change, React renders the component and compares the results to the previous render. The render results will be different due to the new currentTime
and React will update the DOM accordingly.
How can we modify this component to limit what needs to be rendered each time?
We can split out TrackInfo
since it is not dependent on changes to currentTime
.
const TrackInfo = ({ title, artist }) => {
return (
<>
<div>Track Title: {title}</div>
<div>Artist: {artist}</div>
</>
)
}
const CurrentTrack = ({ title, artist, duration, currentTime }) => {
return (
<div>
<TrackInfo title={title} artist={artist} />
<div>
{currentTime} / {duration}
</div>
</div>
)
}
export default CurrentTrack
Now when currentTime
changes, React will render CurrentTrack
and compare the results to the previous render. Since they are different it will update the DOM, same as above.
So did we benefit from splitting TrackInfo
into it’s own component?
Not much. React will still render TrackInfo
on each change of currentTime
, but since the render result is the same, React won’t update the DOM.
This is where React.memo
comes in. Let’s memoize TrackInfo
.
const TrackInfo = ({ title, artist }) => {
return (
<>
<div>Track Title: {title}</div>
<div>Artist: {artist}</div>
</>
)
}
const MemoizedTrackInfo = React.memo(TrackInfo)
const CurrentTrack = ({ title, artist, duration, currentTime }) => {
return (
<div>
<MemoizedTrackInfo title={title} artist={artist} />
<div>
{currentTime} / {duration}
</div>
</div>
)
}
export default CurrentTrack
Since we wrapped TrackInfo
with React.memo
, we are telling React to only render TrackInfo
if the props have changed.
As long as title
and artist
remain the same, React will use the last memoized result instead of rendering again.
Benefits of Memoizing in React
When using a memoized component, React can skip the rendering and comparison work that it would ordinarily do.
In the example above, we prevent having to render TrackInfo
on most changes to CurrentTrack
.
This becomes more beneficial when the memoized component is doing a lot of work to render, such as rendering many subcomponents or performing a lot of computation.
When to use Memo?
So should you use memo on every component?
No.
Memo adds complexity to your code. It’s best to only use memo when there is a measurable performance improvement from it.
A great time to consider using memo is when a component frequently renders with the same props. If a component’s props are often changing, memo won’t provide a benefit.
To better understand where you may benefit from memoization, it’s helpful to profile your application. For more info see:
- Profiling Components from the React docs.
- Introducing the React Profiler post from the React Blog.
- Profile a React App for Performance by Kent C. Dodds.
Memo, Hooks, and State
Memo only applies to changes in props.
It’s safe to use memo on a functional component that implements hooks or updates internal state. A change to state will still cause a rerender, even when the component is wrapped with React.memo
.
Custom Props Comparison
By default, memo will only do a shallow comparison of props. This can cause confusion if the props you’re relying on for rendering are nested within an object.
You can provide your own comparison function as the second argument to the memo function to tell React when props have changed.
For example, assume you have a component that takes in a post
object.
If the post changes, memo will work as expected since a new object is passed in. But what if only the title changes and the object remains the same? React.memo
won’t know that the props have changed and won’t rerender.
A custom comparison function can fix this. For example:
const BlogPost = props => {
return (
<div>
<h1>{props.post.title}</h1>
{props.post.content}
</div>
)
}
const areEqual = (prevProps, nextProps) => {
if (
prevProps.post.title === nextProps.post.title &&
prevProps.post.content === nextProps.post.content
) {
return true
}
return false
}
export default React.memo(BlogPost, areEqual)
Conclusion
Hopefully you have a better grasp on how to use memo to prevent extra rendering!
If you have any questions, comments, or corrections please email me at mike@mguida.com.
Written by Mike Guida who enjoys prolonged outdoor adventures and building stuff with software. Follow him on Twitter