Optimization Scheme for React Component Rendering

    Nov 29, 2021

    React.memo

    React.memo is a component level cache API in React, which can temporarily store rendered components, and when necessary, decide whether to update the rendering or obtain it directly from the cache to avoid unnecessary component rendering.

    And its' usage:

    const MyMemoedComponent = React.memo(
      function MyComponent(props) {
        /* render with props */
      },
      function compare(prevProps, currentProps) {
        /* logics for comparing the prevProps and nextProps */
      },
    );
    

    It returns a Memorized function component.

    When used, it accepts two parameters: the first is the target component, which is the definition of the component that needs to be optimized. The second is the comparison function compare, which receives the currentProps passed in for each round of rendering and the prevProps passed in for the last rendering.

    However, due to the immutable feature in the React function component, the component will be rendered according to its Props. However, when the same or equivalent Props are used multiple times during rendering, performance will be wasted.

    Users can write their own logic to determine whether the two incoming props are logically equivalent. The compare function is expected to return a boolean value. If it returns true, it means that the props passed in twice are equivalent. Then during the current round of rendering, the last component will be directly taken out of the cache without re-rendering the component.

    For example, when it is necessary to render a personal resume information, if the personal information on the resume remains unchanged, then there is no need to re-render. Can be used like this:

    const MyMemoedProfile = React.memo(
      function Profile({ name, sex, age, job }) {
        return (
          <div>
            <h2>{`Your Name: ${name}`}</h2>
            <ul>
              <li>{`sex: ${sex}`}</li>
              <li>{`age: ${age}`}</li>
              <li>{`job: ${job}`}</li>
            </ul>
          </div>
        );
      },
      function compare(prevProps, currentProps) {
        return (
          prevProps.name === currentProps.name &&
          prevProps.sex === currentProps.sex &&
          prevProps.age === currentProps.age &&
          prevProps.job === currentProps.job
        );
      },
    );
    

    useMemo

    The React.memo mentioned above is the optimization of the component level, then useMemo is the optimization of the internal calculation logic of the component.

    JavaScript is not suitable for intensive calculations. But if we have calculation-intensive logic inside the component, every time the component is rendered, the calculation must be done again, which is also unacceptable for performance. Therefore, useMemo can temporarily store the intensive calculation results of each time, and then decide whether to repeat the calculation according to the judgment.

    The specific usage is as follows:

    const memoizedValue = useMemo(() => doSomeComputeExpensiveWork(a, b, ...), [a, b, ...]);
    

    Unlike React.memo, its first parameter is a function that returns a computationally intensive function. It accepts dependencies instead of comparison functions as its second argument. Dependencies will be compared (shallow comparison here) every time a value is fetched. If the dependencies are the same, the last rendering result will be reused instead of recalculated. If the dependencies are different, call the computationally intensive function returned by the first argument.

    For example, we want to design a component that displays the Fibonacci sequence: input the number of items n, and return the Fibonacci number of the nth item.

    function FibonacciNumber({ n }) {
      const target_value = useMemo(
        () =>
          function calc(n) {
            let arr = [];
            if (n < 1) return arr;
            let one = 1,
              two = 0,
              three = 0;
            for (let i = 1; i <= n; i++) {
              three = one + two;
              arr.push(three);
              one = two;
              two = three;
            }
            return arr;
          },
        [n],
      );
      return <div>{`The ${n} th's fibonacci number is : ${target_value}`}</div>;
    }
    

    In this way, no matter how many rounds the component renders, as long as the dependency n remains unchanged, the previously calculated Fibonacci numbers will be obtained from the cache instead of recalculated. If a new n is obtained, the calc function returned by the first parameter will be used again to calculate the new value with n as the parameter.

    If the dependency is empty, the data is always fetched from the cache.

    useCallback

    useMemo has a problem: the function in the first argument must return a computationally intensive function. If the first parameter is directly set to a calculation-intensive function, it will be simplified a lot, as long as the calculation-intensive function does not need to be changed.

    useCallback can be regarded as the equivalent grammatical sugar of useMemo, and there is the following conversion relationship between them:

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

    Like useMemo, if the dependency is empty, the data is always fetched from the cache.

    useEvent

    Try the following code:

    const MyComponent = () => {
      const [counter, setCounter] = useState(0);
    
      const handleClick = () => {
        console.log(counter);
        setCounter((counter) => counter + 1);
      };
    
      return <div onClick={handleClick}>click to add counter counter: {counter}</div>;
    };
    

    There seems to be no problem with this component, but if you think about it carefully, a new handlerClick event function will be recreated every time it is rendered and then passed to the button. So if the component is rendered many times, the handlers established in history will accumulate more and more.

    In order to solve this problem, we put a layer of useCallback to cache the event forever, and always get data from the cache in each round of rendering:

    const MyComponent = () => {
      const [counter, setCounter] = useState(0);
    
      const handleClick = useCallback(() => {
        console.log(counter);
        setCounter((counter) => counter + 1);
      }, []);
    
      return <div onClick={handleClick}>click to add counter counter: {counter}</div>;
    };
    

    But this triggers React's closure trap. The counter in the function in the first parameter is always the value when the component is first created, that is 0.

    So useEvent solves the problem:

    const MyComponent = () => {
      const [counter, setCounter] = useState(0);
    
      const handleClick = useEvent(() => {
        console.log(counter);
        setCounter((counter) => counter + 1);
      });
    
      return <div onClick={handleClick}>click to add counter counter: {counter}</div>;
    };
    

    But it is still an experimental feature today and not recommended for production use.