浅谈React性能优化

avatar

为什么要做性能优化?

示例1:

import React, { useState } from "react";
export default function App() {
  const [num, updateNum] = useState(0);
  console.log('App render啦');
  return (
    <div style={{ backgroundColor: '#2196f3', width: '50%', padding: '20px', marginRight: '20px' }}>
     <h2>组件1的num:{num}</h2>
     <button onClick={() => {updateNum(num + 1);}} style={{ marginBottom: '10px' }}>点我+1</button>
      <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
        <Child1 />
        <Child2 />
      </div>
    </div>
  );
}

const Child1 = () => {
  console.log('组件1-1 render啦')
  return (
    <div style={{ backgroundColor: '#ffeb3b', height: '300px', width: '50%', marginRight: '20px' }}>
      组件1-1
    </div>
  )
}
const Child2 = () => {
  console.log('组件1-2 render啦')
  return (
    <div style={{ backgroundColor: '#ffeb3b', height: '300px', width: '50%' }}>
      组件1-2
    </div>
  )
}

触发一次按钮之后我们会发现child1和child2的console.log的内容都打印了

image.png 这是因为React项目中的任何一个组件发生state状态的变更,React更新机制都会从最顶层的根节点开始往下递归对比,通过双缓存机制判断出哪些节点发生了变化,然后更新节点。这样的更新机制成本并不小,因为在判断过程中,如果React发现 props、state、context任意一个不同,那么就认为该节点被更新了,对比的成本非常小,但是 re-render 的成本偏高。但其实child1和child2中并没有需要更新的内容,这两个组件的渲染是非必要的,他们两个的更新都是有耗时的,存在性能浪费,所以我们要做些性能优化,去避免不必要的渲染。

怎样让他只更新变化了的组件,跳过没有变化的组件?

上面说React发现 props、state、context任意一个不同,那么就认为该节点被更新了,那我们是否可以认为props、state、context是会变化的部分,变化的部分会影响不变的,那我们把变化的部分和不变的部分拆分,让他们互不影响,是否能够做到性能优化呢?

import React, { useState } from "react";
export default function App() {
  const [num, updateNum] = useState(0);
  console.log('App render啦');
  return (
    <div style={{ backgroundColor: '#2196f3', width: '50%', padding: '20px', marginRight: '20px' }}>
      <UpdateNumCom />
      <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
        <Child1 />
        <Child2 />
      </div>
    </div>
  );
}
const UpdateNumCom = () => {
  console.log('num组件render啦');
  const [num, updateNum] = useState(0);
  return <>
           <h2>组件1的num:{num}</h2>
           <button onClick={() => { updateNum(num + 1); }} style={{ marginBottom: '10px' }}>点我+1</button>
         </>
}
const Child1 = () => {
  console.log('组件1-1 render啦')
  return (
    <div style={{ backgroundColor: '#ffeb3b', height: '300px', width: '50%', marginRight: '20px' }}>
      组件1-1
    </div>
  )
}
const Child2 = () => {
  console.log('组件1-2 render啦')
  return (
    <div style={{ backgroundColor: '#ffeb3b', height: '300px', width: '50%' }}>
      组件1-2
    </div>
  )
}

image.png 发现只有UpdateNumCom组件更新了,其他组件都没有更新,这样拆分的变与不变的部分是可以达成性能优化的。

示例2:

import React, { useState } from "react";
export default function Demo1 (){
  console.log('Demo1 render');
  const [num, updateNum] = useState(0);
  return (
    <div title={num + ''}>
    <input value={num} onChange={(e)=> updateNum(Number(e.target.value)) } />
    <p>num is {num}</p>
    <Child1 />
  </div>
  );
}
function Child1(){
  console.log('子组件 render');
  return <p>一个组件</p>;
}

image.png 最外层div的titile依赖num,这样我们怎样拆分变与不变的部分呢?

import React, { useState } from "react";
export default function Demo1 (){
  console.log('Demo1 render');
  return (
    <Input>
      <Child1 />
    </Input>
  );
} 
function Child1(){
  console.log('子组件 render');
  return <p>一个组件</p>;
}
function Input({children}){
  const [num, updateNum] = useState(0);
  return (
    <div title={num + ''}>
      <input value={num} onChange={(e)=> updateNum(Number(e.target.value)) } />
      <p>num is {num}</p>
      {children}
    </div>
  );
}

我们可以通过children的方式传参,我们发现这样也可以实现性能优化,这是因为useState是在Input组件中,Demo1没有state(变化的部分),Input内传入的props也不会变化,子孙组件内也没有state不会重新渲染。

通过上面的例子我们发现,不使用性能优化Api我们也可以通过优化组件的手段实现性能优化

但其实我们日常生活中更多的是使用API去实现性能优化,但是什么情况下使用什么API呢,下面介绍一下几个API

性能优化API

React.memo

memo 接受两个参数:

WrapComponent:你要优化的组件;

(prev, next) => boolean:通过对比 prev(旧 props),next(新 props)是否一致,返回 true(不更新)、false(更新);

注意:memo 只针对 props 来决定是否渲染,且是浅比较 示例:

import React, { useState, useCallback } from 'react';
import iEqual from 'fast-deep-equal/react';

const Children = ({value}) => {
  console.log('Children render');
  return <div>{JSON.stringify(value)}</div>
};

const ChildrenMemo = React.memo(Children, (newProps, oldProps) => iEqual(newProps, oldProps));
export default function Home() {
  const [count, setCount] = useState({a:1});

  const onClick = useCallback(() => {
    setCount({a:2});
  },[]);

  console.log('Home render');

  return (
    <div>
      <button onClick={onClick}>button {JSON.stringify(count)}</button>
      <ChildrenMemo value={count}/>
    </div>
  );

}

useCallback

useCallback 同样接受两个参数:

callback:传入子组件的函数 deps:相关依赖项数组

最终 useCallback 会把传入的 callback 缓存起来。当 deps 依赖发生改变的时候,会重新缓存最新的 callback ,否则就使用缓存的结果

import React, { useState, useCallback } from 'react';
import iEqual from 'fast-deep-equal/react';

const Children = ({value, onClick}) => {
  console.log('Children render');
  console.log(onClick,'onClick');
  return <div>{JSON.stringify(value)}</div>
};

const ChildrenMemo = React.memo(Children, (pre, next)=>{
  console.log(pre, next)
return pre === next;
});
export default function Home() {
  const [count, setCount] = useState(1);

  const onClick =() => {
    setCount(count + 1);
  };
  console.log('Home render');
  return (
    <div>
      <button onClick={onClick}>button {JSON.stringify(count)}</button>
      <ChildrenMemo value={count} onClick={onClick}/>
    </div>
  );
}

useMemo

useMemo 接受两个参数:

callback:计算结果的执行函数 deps:相关依赖项数组

最终 useMemo 在执行了 callback 后,返回一个结果,这个结果就会被缓存起来。当 deps 依赖发生改变的时候,会重新执行 callback 计算并返回最新的结果,否则就使用缓存的结果。

在日常我们经常会使用上面三个性能优化的API,但是我们为什么可以通过这几个Api实现性能优化呢

性能优化背后的源码运行机制

上面说React发现 props、state、context任意一个不同,那么就认为该节点被更新了,那是怎么进行对比的呢,通过react源码我们来看一下内部是怎么实现的

function beginWork(current, workInProgress, renderLanes) {
   
    if (current !== null) {
      var oldProps = current.memoizedProps;
      var newProps = workInProgress.pendingProps;

      if (oldProps !== newProps || hasContextChanged() || (workInProgress.type !== current.type )) {
        didReceiveUpdate = true;
      } else {
        // Neither props nor legacy context changes. Check if there's a pending
        // update or context change.
        var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);

        if (!hasScheduledUpdateOrContext && // If this is the second pass of an error or suspense boundary, there
        // may not be work scheduled on `current`, so we check for this flag.
        (workInProgress.flags & DidCapture) === NoFlags) {
          // No pending updates or context. Bail out now.
          didReceiveUpdate = false;
          return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
        }

        if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
         
          didReceiveUpdate = true;
        } else {
         
          didReceiveUpdate = false;
        }
      }
    } else {
      didReceiveUpdate = false;

      if (getIsHydrating() && isForkedChild(workInProgress)) {
       
        var slotIndex = workInProgress.index;
        var numberOfForks = getForksAtLevel();
        pushTreeId(workInProgress, numberOfForks, slotIndex);
      }
    } 
    workInProgress.lanes = NoLanes;

    switch (workInProgress.tag) {
      case IndeterminateComponent:
        {
          return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
        }

      case LazyComponent:
        {
          var elementType = workInProgress.elementType;
          return mountLazyComponent(current, workInProgress, elementType, renderLanes);
        }

      case FunctionComponent:
        {
          var Component = workInProgress.type;
          var unresolvedProps = workInProgress.pendingProps;
          var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
          return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
        }

      case ClassComponent:
        {
          var _Component = workInProgress.type;
          var _unresolvedProps = workInProgress.pendingProps;

          var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps);

          return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes);
        }

      case HostRoot:
        return updateHostRoot(current, workInProgress, renderLanes);

      case HostComponent:
        return updateHostComponent(current, workInProgress, renderLanes);

      case HostText:
        return updateHostText(current, workInProgress);

      case SuspenseComponent:
        return updateSuspenseComponent(current, workInProgress, renderLanes);

      case HostPortal:
        return updatePortalComponent(current, workInProgress, renderLanes);

      case ForwardRef:
        {
          var type = workInProgress.type;
          var _unresolvedProps2 = workInProgress.pendingProps;

          var _resolvedProps2 = workInProgress.elementType === type ? _unresolvedProps2 : resolveDefaultProps(type, _unresolvedProps2);

          return updateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes);
        }

      case Fragment:
        return updateFragment(current, workInProgress, renderLanes);

      case Mode:
        return updateMode(current, workInProgress, renderLanes);

      case Profiler:
        return updateProfiler(current, workInProgress, renderLanes);

      case ContextProvider:
        return updateContextProvider(current, workInProgress, renderLanes);

      case ContextConsumer:
        return updateContextConsumer(current, workInProgress, renderLanes);

      case MemoComponent:
        {
          var _type2 = workInProgress.type;
          var _unresolvedProps3 = workInProgress.pendingProps; // Resolve outer props first, then resolve inner props.

          var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);

          {
            if (workInProgress.type !== workInProgress.elementType) {
              var outerPropTypes = _type2.propTypes;

              if (outerPropTypes) {
                checkPropTypes(outerPropTypes, _resolvedProps3, // Resolved for outer only
                'prop', getComponentNameFromType(_type2));
              }
            }
          }

          _resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
          return updateMemoComponent(current, workInProgress, _type2, _resolvedProps3, renderLanes);
        }

      case SimpleMemoComponent:
        {
          return updateSimpleMemoComponent(current, workInProgress, workInProgress.type, workInProgress.pendingProps, renderLanes);
        }

      case IncompleteClassComponent:
        {
          var _Component2 = workInProgress.type;
          var _unresolvedProps4 = workInProgress.pendingProps;

          var _resolvedProps4 = workInProgress.elementType === _Component2 ? _unresolvedProps4 : resolveDefaultProps(_Component2, _unresolvedProps4);

          return mountIncompleteClassComponent(current, workInProgress, _Component2, _resolvedProps4, renderLanes);
        }

      case SuspenseListComponent:
        {
          return updateSuspenseListComponent(current, workInProgress, renderLanes);
        }

      case ScopeComponent:
        {

          break;
        }

      case OffscreenComponent:
        {
          return updateOffscreenComponent(current, workInProgress, renderLanes);
        }
    }
  }
function updateSimpleMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
    {
      if (workInProgress.type !== workInProgress.elementType) {
       
        var outerMemoType = workInProgress.elementType;

        if (outerMemoType.$$typeof === REACT_LAZY_TYPE) {
         
          var lazyComponent = outerMemoType;
          var payload = lazyComponent._payload;
          var init = lazyComponent._init;

          try {
            outerMemoType = init(payload);
          } catch (x) {
            outerMemoType = null;
          } // Inner propTypes will be validated in the function component path.


          var outerPropTypes = outerMemoType && outerMemoType.propTypes;

          if (outerPropTypes) {
            checkPropTypes(outerPropTypes, nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
            'prop', getComponentNameFromType(outerMemoType));
          }
        }
      }
    }

    if (current !== null) {
      var prevProps = current.memoizedProps;

      if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress.ref && ( // Prevent bailout if the implementation changed due to hot reload.
       workInProgress.type === current.type )) {
        didReceiveUpdate = false; // The props are shallowly equal. Reuse the previous props object, like we
       
        workInProgress.pendingProps = nextProps = prevProps;

        if (!checkScheduledUpdateOrContext(current, renderLanes)) {
          
          workInProgress.lanes = current.lanes;
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
          // This is a special case that only exists for legacy mode.
          // See https://github.com/facebook/react/pull/19216.
          didReceiveUpdate = true;
        }
      }
    }

    return updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes);
  }
function shallowEqual(objA, objB) {
    if (objectIs(objA, objB)) {
      return true;
    }

    if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
      return false;
    }

    var keysA = Object.keys(objA);
    var keysB = Object.keys(objB);

    if (keysA.length !== keysB.length) {
      return false;
    } // Test for A's keys different from B.

    for (var i = 0; i < keysA.length; i++) {
      var currentKey = keysA[i];

      if (!hasOwnProperty.call(objB, currentKey) || !objectIs(objA[currentKey], objB[currentKey])) {
        return false;
      }
    }

    return true;
  }

通过打断点发现组件更新进入beginWork中,会判断oldProps !== newProps || hasContextChanged() || workInProgress.type !== current.type,不相等的话会继续更新,相等则不更新,使用memo则会进入SimpleMemoComponent判断,执行updateSimpleMemoComponent方法,执行shallowEqual通过objectIs进行新旧props的对比,如果相同则不会更新,这里需要注意的是如果props是对象,他只会比较第一层的key下的value是否相等。

之前我们通过组件优化或者使用API的形式都可以实现性能优化,现在react19出现,React Compiler 可以让我们不需要手动去进行性能优化,内部便实现了性能优化。

以上是我自己对react性能优化的一些学习,可能表述不是很正确,大家一起沟通学习。