In this article, we will clarify how to use useMemo
and useCallback
to optimize performance in React application.
Problem
Do useMemo
and useCallback
really help optimize performance in React App or do they just make things worse?
How it works
useCallback
useCallback
is a Hook introduced in React since version 16.8. useCallback
returns a function and an array containing dependencies (variables that are passed from outside to which the function is dependent upon run). useCallback
uses a memorization mechanism – remembers the result of a function into memory and returns the function to be remembered in case the dependencies do not change.
1 2 3 4 | const myFunction = useCallback(() => { //do someting },[dependencies]) |
Why use useCallback?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const ParentComponent = () => { const [count, setCount] = useState(0); const loggingStatus = () => { console.log("Run from ChildComponent"); }; const addMore = () => { setCount(prev => prev + 1); }; return ( <div> <p>Current: {count}</p> <ChildComponent loggingStatus={loggingStatus} /> <button onClick={addMore}>Click</button> </div> ); }; const ChildComponent = ({ loggingStatus }) => { useEffect(() => { loggingStatus(); }, [loggingStatus]); return <div />; }; |
Notice the example above, we have ParentComponent
and ChildComponent
. When you click the button in ParentComponent
, this component will be re-rendered due to the state
change. As a result, ChildComponent
was also re-rendered even though there was no change (props passed in the integer).
useCallback
was born to solve this problem. It will remember the function passed and the list of dependencies. Now, when the button is clicked, React will re-render the ParentComponent
and compare whether the ChildComponent
props have changed for further rendering. Because loggingStatus
completely independent of any dependency, its value does not change and ChildComponent
will not be re-rendered.
Compare two cases
1 2 3 4 5 | // Với useCallback const loggingStatus = useCallback(() => { console.log("Run from ChildComponent"); },[]); |
1 2 3 4 5 | // Không dùng useCallback const loggingStatus = () => { console.log("Run from ChildComponent"); }; |
What is the cost of using useCallback
? If we leave the two useCallback
stand alone, we can be sure that useCallback
will consume more memory, namely:
useCallback
must remember the function passed into ituseCallback
must initialize an array containing the dependencies and remember them
More specifically, on the second component rendering, loggingStatus
will be removed from memory and re-initialized. However, with useCallback
, we have to just create a new function, and remember the old function of the first render. The same thing happens with dependency arrays. From the performance perspective of memory, it’s clear that useCallback
consumes more resources.
how about useMemo?
useMemo
has the same mechanism of operation as useCallback
, but instead of passing a function, we can pass a function that returns every type of data we want to remember:
1 2 3 4 5 | const myFruit = ['apple', 'orange', 'banana', 'watermelon'] const initialMyFruit = React.useMemo( () => ['apple', 'orange', 'banana', 'watermelon'], []) |
initialMyFruit
will only be initialized in case the value of a dependency changes.
Summary
Everything has its price, so we need to use useCallback
and useMemo
wisely to avoid being laughed at by our colleagues.
When to use useCallback and useMemo?
There are two cases as follows:
- Referential equality
- These calculations are heavy
Referential equality
Since there is no word in Vietnamese synonymous with “Referential equality”, I will explain this concept a bit. How do React know if there are changes in dependencies, state or props to re-render components? React will compare the following:
1 2 3 4 5 6 7 8 9 10 | true === true // true false === false // true 1 === 1 // true 'a' === 'a' // true {} === {} // false [] === [] // false () => {} === () => {} // false const z = {} z === z // true |
It is worth noting that objects, arrays and functions contain the same content but Javascript still understands that these are two different phenomena. For example, you and your neighbor have the same Mercedes-Maybach S650, even though the two cars are exactly the same, it is impossible to understand that the neighbor’s car is yours.
Similarly, when initializing 2 objects, arrays or functions, even though their contents are identical, they are essentially a different memory area, so they will still be two different phenomena.
There are two cases in which React would use Referential equality:
Dependencies lists
Returning to the example above with a little change:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | const ParentComponent = () => { const [count, setCount] = useState(0); const myObject = { name: "Minh", age: 23 }; const addMore = () => { setCount(prev => prev + 1); }; return ( <div> <button onClick={addMore}>Click</button> <p>Count: {count}</p> <ChildComponent myObject={myObject} /> </div> ); }; const ChildComponent = ({ myObject }) => { const { name, age } = myObject; useEffect(() => { console.log(myObject); }, [myObject]); return <div>{`Tên:${name} ${age} tuổi `}</div>; }; |
This time, we initialize myObject
from ParentComponent
and pass it to ChildComponent
via props. In addition, useEffect
is used to run console.log(myObject)
whenever a change from the props is passed. When clicking the Click button at ParentComponent, although there was no change in myObject
, ChildComponent
was still rendered. The reason for this is the Referential equality mechanism of JavaScript.
This is when useMemo is used:
1 2 3 4 5 | const myObject = useMemo(() => ({ name: "Minh", age: 23 }),[]); |
Heavy calculations, graphs, animation
In fact, in most cases we won’t need to worry about re-render components because React has a very fast processing speed and this optimization will not make too much difference. However, there is a case, useEffect
and useMemo
should be used to restrict the initialization & execute functions “consume CPU” as shown below:
1 2 3 4 5 6 7 8 9 10 | function mySlowFunction(baseNumber) { console.time('mySlowFunction'); let result = 0; for (var i = Math.pow(baseNumber, 7); i >= 0; i--) { result += Math.atan(i) * Math.tan(i); }; console.timeEnd('mySlowFunction'); } mySlowFunction(8); |
Conclude
Using APIs like useMemo
or useCallback
to optimize code is nothing bad. However, overusing “code optimization” without really understanding the problem can make code performance worse. In addition, concepts like useMemo
and useCallback
can make code complex and difficult to read, reducing the effectiveness of working with team members. My advice is not to mechanically optimize, really understand and apply these APIs appropriately in different situations.
Reference & credits
This article references the ideas of some other articles: