Memoising Components
Following on from the previous section about memoising values, this section will discuss the use of React.memo
and when it's best employed. Minimising component re-renders is often necessary to improve performance and reduce frame drops, but unfortunately it's not as simple as just wrapping everything in React.memo
, setting your Slack status as away and heading to the local for a few frothies.
For a recap on React component memoisation read this part of the groundwork section.
Understanding React.memo
React determines what to render by taking the output of a component render, comparing it to the previous render, and checking for a difference. If there's a difference, it means the UI has changed and React commits that difference. For the most part React is really good at this, and can do it pretty fast. When it can't though, we can wrap a component in React.memo
and tell React to memoise the result of the initial render, and if on the next render the props are the same skip it.
To better illustrate this, here is an example:
export const RaceEntrant = ({ name, id }) => (
<View>
<Text>{name}</Text>
<Text>{id}</Text>
</View>
);
export const MemoisedRaceEntrant = React.memo(RaceEntrant);
Here we have declared a component RaceEntrant
, and a memoised version of that same component MemoisedRaceEntrant
. The first component will re-render regardless of whether or not the props change, whereas the memoised component will perform a comparison of its props and only render if they have changed.
// The invocation of React.createElement will only occur on initial render - after that the initial result is reused
<MemoisedRaceEntrant name="Steve" id={123} />
In this snippet, React will skip the re-render of the component because the props are hardcoded primitive values and won't change across renders. This can give us a performance improvement because React is doing less work for the same result.
By default React.memo
only performs a shallow equality comparison of the props object. It's possible to pass a second argument to React.memo
and implement your own custom equality comparison logic. In this example MemoisedRaceEntrant
will only re-render if the name
prop has changed, regardless of the equality of the rest of the props.
const areEqual = (prevProps, nextProps) => {
return prevProps.name === nextProps.name;
};
export const MemoisedRaceEntrant = React.memo(RaceEntrant, areEqual);
Common pitfalls
Passing referentially unstable props
Where many people go wrong is in failing to remember that the shallow equality comparison performed by React.memo
will return false if the reference for non-primitive values has changed. As discussed in the previous section [] === []
is false, so any dependency on that value will re-run even if the internal values have stayed the same.
JS doesn't look at [1, 2, 3] === [1, 2, 3]
and compare the 1, 2 and 3
, it compares the address in memory of both arrays to determine if it's the same array.
In each example below, the respective memoised component will re-render each time because each render of ParentComponent
will recreate the prop value, assigning it a new reference, and React.memo
will evaluate that and determine it has changed triggering a re-render. This same logic applies to any non-primitive types - functions, arrays and objects.
const ParentComponent = () => {
// BAD - the inline array will be re-created each render defeating optimisation
return <MemoisedList items={[1, 2, 3]} />;
};
const MemoisedList = React.memo(({ items }) => {
return <FlatList data={items} />;
});
const ParentComponent = () => {
// BAD - the inline function will be re-created each render defeating optimisation
return <MemoisedButton onPress={() => console.log("PRESSED")} />;
};
const MemoisedButton = React.memo(({ onPress }) => {
return <Button onPress={onPress} />;
});
const ParentComponent = () => {
// BAD - the inline object will be re-created each render defeating optimisation
return <MemoisedCard entrant={{ name: "Steve", id: 123 }} />;
};
const MemoisedCard = React.memo(({ entrant }) => {
return (
<View>
<Text>{entrant.name}</Text>
<Text>{entrant.id}</Text>
</View>
);
});
Each of these could be resolved by wrapping the offending value in useMemo
or useCallback
, or by moving it into the outer scope of the file.
Memoising components that receive children
Another common issue overlooked is using React.memo
on a component that receives children
. This is often easily overlooked, but makes sense once you think about it. JSX tags are translated into React.createElement
calls which return new React element instances. Being objects it makes sense they have a different reference, and that the shallow equality comparison will fail.
const ParentComponent = () => {
return (
<MemoisedComponent>
<View>
<Text>I'm a child!</Text>
</View>
</MemoisedComponent>
);
};
const MemoisedComponent = React.memo(({ children }) => {
return <View>{children}</View>;
});
In this example React.memo
isn't really doing anything because each equality check will inevitably fail when it compares children
. In cases like this it's going to take optimisations in other parts of the app to achieve any performance improvements.
When to use React.memo
The best candidate for React.memo
is a component that is expected to render often and with the same props. A good example of this is child components consisting of a number of "leaf" components (also known as dumb components, the elements at the end of the render tree such as Text
) that will remain largely unchanging throughout the lifecycle of a component.
It's also a good decision to memoise when a component is fairly large and computationally expensive, and minimising re-renders is sure to offer performance improvements.
Generally memoisation is better applied higher up in the component tree. This means that rather than wrapping every little presentational component, or atom
as as they are called in atomic design, the memoisation should be applied to the component that is built from them.
// Prefer to memoise this
const Card = React.memo(() => {
return (
<View>
<Image />
<Divider />
<Name />
<Divider />
<Footer />
</View>
);
});
// Not necessarily this
const Name = ({ name }) => {
return <Text>{name}</Text>;
};
// or this
const Divider = () => {
return <View style={{ height: 5 }} />;
};
Component trees generally terminate in these "leaf" components, hence the name. When React decides to skip a re-render it will also mean that all of its children won't re-render. Because of this you will get more bang for your buck when memoising higher up the tree, because you're also preventing re-renders of these smaller, more granular components without having to memoise each and every one of them.
It's important to measure renders and determine if a component will benefit from memoisation, and once it's applied, measure again to make sure it's working as expected. If the performance gains are fairly minimal, it probably isn't worth using React.memo
.
TL:DR
- Fix slow renders before memoising a component.
- Use
React.memo
when a component re-renders often and with the same props. - Generally better to use
React.memo
on larger (computationally expensive) components, and components that are higher up the tree. - Make sure a memoised component is receiving referentially stable props from the point of that prop's declaration.
- Measure before and after memoisation - it's important to be able to justify why something is memoised.