Under the React framework, the Function Component must repeatedly perform expensive calculations to get the final Render Tree. The React development team is aware of this problem and has developed some optimizations that use internal caching to reduce repetitive code execution and improve component performance. However:

Performance tuning always has costs and does not always bring benefits.

This article will discuss the costs and benefits of useMemo, useCallback, and Memo, and give examples of how to open useMemo, useCallback, and Memo correctly.

quotes

Take a look at this simple code:

import React from 'react';

export default function Main() {
  const data = ['apple'.'banana'.'pears'.'watermelon'.'coconut'.'mangosteen'];

  return (
    <div className={'main'} >
      <ul>
        {data.map((text) => (
          <li>{text}</li>
        ))}
      </ul>
    </div>
  );
}
Copy the code

The code above implements a dynamic UL list, and to “improve” performance, we refer to useMemo. Wrapping data with useMemo allows you to cache data and prevent it from being defined twice.

import React, { useMemo } from 'react';

export default function Main() {
  const data = useMemo(() = > {
    return ['apple'.'banana'.'pears'.'watermelon'.'coconut'.'mangosteen']; } []);return (
    <div className={'main'} >
      <ul>
        {data.map((text) => (
          <li>{text}</li>
        ))}
      </ul>
    </div>
  );
}
Copy the code

So my question is, in this particular example, which is better for performance? Original OR after using useMemo?

If you chose useMemo, congratulations, you are wrong. The answer is: Better performance with the old code!

????? UseMemo is a recommended optimizer for React. How can it cause performance degradation?

Let’s talk about it…

useMemo

Let’s take a look at the definition:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Copy the code

useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to Avoid expensive calculations on every render. UseMemo recalculates the value of the memory only when one of the dependencies changes. This optimization helps avoid expensive calculations per render.

UseMemo is a React hook that remembers the output of a function. UseMemo takes two arguments: a function and a dependency list. UseMemo calls this function and returns its return value. Therefore, the optimization idea of useMemo is reference caching. UseMemo is implemented differently in the mount and Update phases, where mountMemo

is executed in the mount phase of the component:

function mountMemo<T> (
  nextCreate: () => T,
  deps: Array<mixed> | void | null.) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

UpdateMemo

executes in the component update phase:

function updateMemo<T> (
  nextCreate: () => T,
  deps: Array<mixed> | void | null.) :T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if(prevState ! = =null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0]; }}}const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

From the above code, we can see that both mountMemo

and updateMemo

have logic to rely on comparison and reference updates. Does this logic take time to execute? The answer is yes! UseMemo will remember the value of the last cb (callback function) operation to reduce the number of double calculations. What if the count takes less time than useMemo itself does the Equals check? Is there a situation where the use of useMemo adds time? Here is a detailed example for analysis:

// T1
const [count, setCount] = useState(0);

// T2
const add = () = > { 
  setCount((prev) = > prev + 1);
};
Copy the code

The code above defines a count, where the add function increments count by one by calling setCount. Assuming that defining count takes T1 and defining add takes T2, the total time of the code above is T1+T2.

The time is defined as T1+T2

Next use useMemo to rewrite:

const [count, setCount] = useState(0);

const memoAdd = useMemo(() = > {
  return () = > {
    setCount((prev) = > prev + 1); }; } []);Copy the code

The code above defines a count, where memoAdd function is a useMemo wrapped function that increments count by 1. The above code is equivalent to the following:

// T1
const [count, setCount] = useState(0);

// T2'
const add = () = > {
  return () = > {
    setCount((prev) = > prev + 1);
  };
};

// T3
const memoAdd = useMemo(add, []);
Copy the code

Compared with the first version without useMemo, the rewrite not only has the time of T1 and T2′, but also increases the time of INTERNAL calculation of useMemo T3.

UseMemo import time: T1+T2’+T3

Which one takes more time? Max(T1+T2, T1+T2’+T3) =? ; Remove the same time T1 from both sides of the expression, so Max(T2, T2’+T3) =? , assuming that no matter how complex the function is, the time of js defining the function is the same, that is, T2 === T2′; Max(T1+T2, T1+T2’+T3) = T1+T2’+T3, T1+T2’+T3 > T1+T2. Using useMemo increases the overall code time!

Of course, the above example uses useMemo to return the result directly, so the running time of the CB function itself is smooched out. But that’s exactly what a lot of developers make in the actual development process. It is clear that it wants to obtain the calculation result of direct return in CB, without any additional complicated calculation, and it also likes to use useMemo to wrap. The result is laborious and thankless, with half the effort!

useCallback

Now that we’re done with useMemo, let’s look at useCallback.

const memoizedCallback = useCallback(
  () = > {
    doSomething(a, b);
  },
  [a, b],
);
Copy the code

UseCallback can return only one callback function.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

According to the official documentation, useCallback is actually the unique useMemo. MountCallback

Is executed in the mount phase of the component:

function mountCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
Copy the code

UpdateCallback

Executes in the component update phase:

function updateCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if(prevState ! = =null) {
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
Copy the code

It can be found that the implementation of useCallback is very similar to useMemo, which means that if the time of obtaining CB is less than the time of useCallback reference equality judgment, the use of useCallback will not improve performance, but increase performance loss.

The key point

Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost. Performance tuning is not free. They always come with costs, and the benefits of optimization don’t always offset the costs.

When useMemo and useCallback

UseMemo and useCallback are built into React.

  1. Guarantee reference equality
  2. Avoid expensive calculations

Guarantee reference equality

The difference between value equality and reference equality is shown in the following example:

// Determine the value to be equal
true= = =true // true
false= = =false // true
1= = =1 // true
'a'= = ='a' // true

// Determine the reference equality= = = {} {}// false[] [] = = =// false() = > {} = = =() = > {} // false

const obj = {}
obj === obj // true
Copy the code

What are the benefits of ensuring reference equality in React? Consider the list of useEffect dependencies. Consider the following example: Component Blub uses component Foo, where component Foo’s useEffect depends on the parameters bar, baz passed in. This looks perfect, the useEffect calculation logic is triggered only when bar and baz change.

function Foo({bar, baz}) {
  React.useEffect(() = > {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz]) // we want this to re-run if bar or baz change
  return <div>foobar</div>
}

function Blub() {
  return <Foo bar="bar value" baz={3} />
}
Copy the code

What if one or both of bar and baz are referential values?

function Blub() {
  return <Foo bar={['bar','value']} baz={() => {}} />
}
Copy the code

Then bar, baz will be new references every time Blub renders, so when React tests the dependency list to see if it changes between renders, it will always evaluate to true, meaning the useEffect callback will be called after each render, Instead of only calling when the values of bar and baz change. The problem has been identified. How to solve it? Then it was the turn of useCallback and useMemo. This type of problem can be solved by reference to memory:

function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz])
  return <div>foobar</div>
}

function Blub() {
  const bar = React.useMemo(() => [1, 2, 3], []);
  const baz = React.useCallback(() => {}, []);
  return <Foo bar={bar} baz={baz} />
}
Copy the code

We use useMemo to wrap BAR and useCallback to wrap Baz to realize the memory of reference changes and effectively avoid the problem of redundant calculation triggered by the rebrush of dependency list caused by inconsistent references during component update.

Tip: The reference caching technique applies not only to useEffect, but also to useLayoutEffect, useCallback, and useMemo dependency list elements.

Avoid expensive calculations

Take a look at the following example (this is just an example of “expensive computation”, not an actual code scenario) :

const [count, setCount] = useState(0);

const add = () = > {
  setCount((prev) = > prev + 1);
};

const memoAdd = useMemo(() = > {
  for (let i = 0; i < 1000000; i++) {
    console.log(i);
  }
  returnadd; } []);Copy the code

It still returns an add method, but adds for (let I = 0; i < 1000000; I++), the execution cost increases exponentially. In this case, because of the cache support of useMemo, the for loop is only executed once in the initial phase and is not executed later, greatly improving performance.

React.memo()

React. Memo () is a new feature introduced in React V16.6. It is similar to the React.PureComponent in that it helps control the re-rendering of function components. React.memo() corresponds to functional components, and react.pureComponent corresponds to class components. There is plenty of documentation available on the web about the basic usage of memo(), which I won’t expand on here. The misunderstandings in the use of memo() will be analyzed and their solutions will be given below.

Let’s start with an example

import React, { useState } from 'react';

interface ButtonProps {
  onClick: (param: any) = > void;
  text: string;
}
const Button = function ({ onClick, text }: ButtonProps) {
  return <button onClick={onClick}>{text}</button>;
};

function NoMemoCandy() {
  const [count, setCount] = useState(0);
  const addCount = () = > {
    setCount((prev) = > prev + 1);
  };
  const clearCount = () = > {
    setCount(0);
  };

  return (
    <div className={'memo-candy'} >
      <div>{count}</div>
      <div className={'button-container'} >
        <Button onClick={addCount} text={'Add'} / >
        <Button onClick={clearCount} text={'Clear'} / >
      </div>
    </div>
  );
}

export default NoMemoCandy;
Copy the code

The code above is a very simple Add/Clear counter, click add counter increment 1, click clear counter zero. Effect:

In the code aboveButtonComponent not in usememoPackage, so every rendering of the parent componentButton(So add and clear here just want to update<div>{count}</div>Section).

Install React Developer Tools and open DevTools in Chrome -> Select Components -> check Highlight Updates when Components Render

You can see that not only the parent component performs render after clicking Add/Clear, but both Button components perform render.

Introducing the memo

Here we have a scene with reduced rendering, and based on what we’ve seen above you will definitely want to introduce the Memo for optimization. Say dry dry ~

.function MemoCandy() {
  // Same as NoMemoCandy, no longer posted here
  const MemoButton = memo(Button);

  return (
    <div className={'memo-candy'} >
      <div>{count}</div>
      <div className={'button-container'} >
        <MemoButton onClick={addCount} text={'Add'} / >
        <MemoButton onClick={clearCount} text={'Clear'} / >
      </div>
    </div>); }...Copy the code

The measure of optimization is usememorightButtonComponents are wrapped to getMemoButtonComponent, and then switch in codeMemoButtonComponent, the rest of the code is exactly the same. After optimization, see the effect:

Uh, uh, the effect is… Nothing! ? Memo doesn’t work as well as it should.

Make the memo really work

Take a look at the official explanation of how memo works:

React.memo only checks the props changes. If a function component is wrapped in react. Memo and its implementation has a Hook for useState, useReducer, or useContext, it will still rerender when state or context changes. By default, only shallow comparisons are performed on complex objects. If you want to control the comparison process, pass in your custom comparison function as a second argument.

Back in our example, the memo checks changes to props using standard JS equality logic: value equality for basic data types and reference equality for reference data types. Therefore, when the component props is a reference type, we know that every render of the parent component will pass a new reference object, and the memo will use the reference equality logic to judge the equality, so that the result will be false every time, so that the render process will not be prevented, resulting in repeated rendering calculation. Now that you know what the problem is, you should focus on the memo component’s reference properties to make sure the references are equal. UseMemo /useCallback makes its debut! In the example above, the onClick property of the MemoButton component is a reference type that is passed in as a value of type react. Dispatch< react. SetStateAction

>. So we can use the useCallback wrapper value to ensure that the reference value is equal after each render:

.function MemoCandy() {...const MemoButton = memo(Button);
  const addCount = useCallback(() = > {
    setCount((prev) = > prev + 1); } []);const clearCount = useCallback(() = > {
    setCount(0); } []);return (
    <div className={'memo-candy'} >
      <div>
        <div>{count}</div>
      </div>
      <div className={'button-container'} >
        <MemoButton onClick={addCount} text={'Add'} / >
        <MemoButton onClick={clearCount} text={'Clear'} / >
      </div>
    </div>); }...Copy the code

The main change to the above code is to wrap the original addCount/clearCount function in useCallback:

const addCount = () = > {
    setCount((prev) = > prev + 1);
  };
const clearCount = () = > {
    setCount(0);
  };
Copy the code

After the optimization:

const addCount = useCallback(() = > {
    setCount((prev) = > prev + 1); } []);const clearCount = useCallback(() = > {
    setCount(0); } []);Copy the code

Look again at the effect:

The two Button components no longer repeat render, perfectly fulfilling our goal.

conclusion

  • Performance tuning comes at a cost. Wait until you really need abstraction or optimization to avoid incurring costs without receiving benefits.
  • useuseCallback 和 useMemoYou’re making your code more complex for your colleagues. You may have made an error in the dependency array, and you may have degraded performance by calling the built-in hooks and preventing the dependency and Memoized values from being garbage collected. These costs are worth it if you get the necessary performance gains, but keep in mindDo not optimize this principle unless necessary.
  • Simple to usememoThe component is memoized, and the real memoized capability of the component is often combineduseCallback 和 useMemoUse.

reference

UseCallback React. Memo When to useMemo and useCallback You’re overusing useMemo: Rethinking Hooks memoization