You often hear people's advice to React developers to implement memoization by using useMemo or useCallback to improve the performance of your React project. Well, the simplest answer is that it reduces the number of re-renders your application has to make for a React component. But the much longer answer requires an understanding of the heap allocation that happens with your computer's memory. The heap is a body of storage where larger blocks of data can persist with a non-deterministic lifecycle as opposed to the stack, which is usually tied to a function or the main process, where the lifecycle is deterministic (tied to the operation of the function).
While the stack is usually very easy to manage by the CPU, managing data on the heap can be very expensive. This can be divided into:
Allocation of data into blocks of memory,
Deallocation of blocks of memory to free them for other users,
Synchronisation, which manages the ownership and usage of the data within certain memory locations across multiple threads to avoid data races and simultaneous allocation/deallocations,
and fragmentation, which involves breaking apart contiguous blocks of memory to fit data of different sizes. This can lead to a lot of free memory blocks that can’t be used.
What programmers need to understand is that all these operations are costly. The programmer needs to decide when and where these costs occur so they don’t have a negative effect on a user's experience with their application.
How does this relate to JavaScript?
JavaScript can be described as an interpreted language, which means the programmer does not have any control over the allocation or deallocation of the heap. Languages like these have their own garbage collectors as opposed to low-level languages like C or C++. React components are simply functions that re-run. That means those costly heap management operations are also going on each rerun. React will rerender a component when it notices differences in its props, state, or changes in a hook. Every time the React function reruns, every variable or function gets a new memory address, which costs cpu cycles and possibly leads to fragmentation.
But there are some sure-fire ways to avoid unintended memory and CPU usage when building your React project. The keyword is Stable references.
Usually, primitive values like strings, integers, or booleans are compared based on their values, but objects are compared based on their memory addresses. Say you pass an inline function as a prop to a child component like so:
function Parent() {
const handleClick = () => console.log("Clicked!");
return <Child onClick={handleClick} />;
}
An inline function creates a new memory reference every time it runs. The child component will constantly re-render because the memory address keeps changing. This is the case with variables that are objects in the Component. They will need to get a memory address on the heap every time the component rerenders.
So, useCallback ensures that the memory address of the functions remain thesame between renders unless necessary,
const handleClick = useCallback(() => { console.log("Clicked!");},[])
useMemo maintains a single memory address for objects or arrays between renders unless necessary.
const object = useMemo(() => ({ color: 'blue' }), []);
However, using memoization too much can be detrimental to your project performance due to the amount of resources used for the diff operations
Ref objects are the ultimate strategy for ensuring stability. These are plain JavaScript objects that will retain the same memory address for the whole lifecycle of the component. A change to the ref value (ref.current) doesn’t trigger a re-render.
const ref = useRef(0)
I am on a mission to understand how to get more value from systems with limited memory and compute resources. This was just me relating some things I learnt back to a framework I use frequently at work. I’d love to hear about any bits of knowledge to better understand the frameworks we use.
Top comments (0)