When your app starts to get slow, it's not a bad idea to start looking into optimizing your code.
This is a sentiment most of us follow (myself included) to avoid falling into the trap of premature optimization.
When I first started using React with hooks, I was very much in the mindset that the memoization hooks (useMemo
and useCallback
) could be spared for this reason. Over time however, after building libraries and applications that use hooks, I've found it almost always makes sense to memoize your code....
Here's why hooks are more than just a performance optimization.
What is a performance optimization
When we optimize code, the intention is to reduce the cost (time or resource usage). Particularly when we're optimizing functions or sections of our app, we don't expect the functionality to change, only the implementation.
The below is an example of a hook we which keeps the same functionality but changes it's implementation.
// Before optimization
const useArrayToObject = (array) => {
return array.reduce((obj, value) => ({ [value]: true }), {});
}
// After optimization
const useArrayToObject = (array) => {
const newCollection = {};
for (let value in array) {
newCollection[value] = true;
}
return newCollection
}
useMemo
as a performance optimization
Now consider we find ourselves using this hook, and despite our prior optimization, we find that we need to further reduce it's cost.
As you can probably guess, we can make use of useMemo
to ensure that the we only run our expensive operation whenever the input argument changes
const useArrayToObject = (array) => {
return useMemo(() => {
const newCollection = {};
for (let value in array) {
newCollection[value] = true;
}
return newCollection
}, [array])
}
We merge the changes with confidence that our new optimization has solved the problem, only to hear later on that it's caused a new bug... but how?
The functional impact of useMemo
Despite intending to make a performance-only optimization by memoizing our hook, we've actually changed the way our app functionally works.
This issue can work both ways - either by adding memoization (sometimes inevitable) or removing it.
Here's the component which was affected by our change
const MyComponent = ({ array, dispatch, ...otherProps}) => {
const collection = useArrayToObject(array);
useEffect(() => {
console.log('Collection has changed'); // Some side effect
}, [collection])
// ...
}
Unlike in the first example, the performance optimizations we have made to the internals of our hook have now changed how consuming components function.
Communicating change
The way in which changes cascade in React hooks is incredibly useful for making a reactive application. But, failing to communicate these changes up front, or modifying when these changes are communicated at a later date, can lead to lost (as in our example) or unintentional reactions elsewhere in your application.
The larger your application and the higher up in your component tree the modifications are, the larger the impact.
Addressing these issues
So now that you understand that useMemo
does more than just optimize performance, here's my suggestion
just get into the habit of memoizing values.
Most are not going to notice the performance impact of additional equality checks incited by over-memoizing; and knowing that change events signalled by values coming from props or hooks can be trusted as actual changes, is valuable.
Update: I've added an example reproduction here demonstrating the functional impact of useMemo
Top comments (1)