react中的性能优化

251 阅读7分钟

前言

说到性能优化,大家一定会想到PureComponent、memo、useMemo、useCallback这些api,比如useMemo,我传入一个依赖项,敌不动我不动,那些复杂计算结果就可以被缓存下来,其实岁月静好是因为有人替你负重前行,哈哈哈。

相比Vue可以基于模版进行编译时性能优化,React作为一个完全运行时的库,只能在运行时谋求性能优化。

正题

不使用api

假如我有一个包含子组件B的父组件A,B是一个不依赖父组件props的组件,父组件A中包含修改自身状态的逻辑,那么每当A组件状态改变,就会带动我不变的B组件更新,如果B组件规模较大,就非常消耗性能。

export default function Optimization() {
    const [num, updateNum] = useState(0)
    return (
        <div>
            <input value={num} onChange={updateNum(num += 1)} />
            <p>num is {num}</p>
            <Expensive />
        </div>
    )
}
function Expensive(){
    let now=performance.now()
    while(performance.now()-now<100){}
    return <p>耗时的组件</p>
}

我们可以在不使用性能优化api的情况下,将变得部分从不变的分出去。

export default function Optimization() {
    return (
        <div>
            <Input/>
            <Expensive />
        </div>
    )
}
function Input() {
    const [num, updateNum] = useState(0)
    return (
        <>
            <input value={num} onChange={updateNum(num += 1)} />
            <p>num is {num}</p>
        </>

    )
}
function Expensive() {
    let now = performance.now()
    while (performance.now() - now < 100) { }
    return <p>耗时的组件</p>
}

书写也更规范了,有效避免造s山哈哈哈。

使用api

  • purecomponent和memo:内部is方法对props浅比较,有一定效果。
  • useMemo和useCallback:如果传递的是引用类型,浅比较可能失效,需要用这类api进行缓存。

以上就是api的效果。

他们似乎都围绕着一些东西在操作,没错,就是围绕react内部的性能优化策略:eager state和bailout

React的工作流程可以简单概括为:

  1. 交互(比如点击事件、useEffect)触发更新
  2. 组件树render

当你更新后的内容与之前无异,就命中eager statet策略,跳过了步骤2。

bailout策略是对于父组件来说的,父组件render之后发现和之前的state相比并没有改变,那么它的子组件就不会被更新,这一过程发生在父组件的步骤2

关于完整的bailout策略过程

  • 父组件render返回的jsx与current fiber tree进行diff,创建workInProgress fiber tree
  • 命中bailout策略:不执行render复用current fiber tree;没有命中:current fiber tree=workInProgress fiber tree
  • render后发现无需更新,然后执行bailout的情况:如shouldComponentUpdate
  • fiber的子树上有work:继续遍历子树;没有:return null
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
 workInProgress: Fiber,
 renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // 重用以前的context依赖关系
    workInProgress.dependencies = current.dependencies;
  }
  // 检测子树(childLanes)是否有work
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    //无,跳过子树
    return null;
  }
  // 说明子树有work,继续遍历子树
  
  // workInProgress.child 转化为 workInProgress
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

执行时机

render前判断

//删除部分代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    // 是update阶段,
    if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      // props不同 需要update
      oldProps !== newProps ||
      // 旧版上下文(现在不使用)
      hasLegacyContextChanged() ||
      // dev时,会判断type,用于热重载
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 需要更新。  (不代表必定更新,例如memo)
      didReceiveUpdate = true;
    } else {
      // 此fiber是否有lanes更新任务
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
      if (
        // 没有updateLane
        !hasScheduledUpdateOrContext &&
        // 没有Suspend,错误边界的传递.
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        didReceiveUpdate = false;
        // 无更新, 尝试bailout. 
        // 多数组件会直接进行baliout。
        // Suspend、Offscreen组件可能不会执行bailout。
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      // 有更新任务,但是props没改变,先设置需要更新为false  
      // 组件后续进行确定更新时,会将其设置为true
      didReceiveUpdate = false;
    }
  } else {
    // mount
  }
  // ...非bailout代码 
}

在beginWork中,当前处于update(有current fiber了)&props不变(===)&没有updateLanes&没有Suspend&NoFlags,尝试进行bailout。

render之前!!!每次render后都会createElement,创建新props对象,就无法命中bailout了。

多数组件可以满足以上条件。

ps:context这种小弟也可能影响命中。

lane无任务

fiber.lane无任务,继续进行bailout

function checkScheduledUpdateOrContext(current: Fiber,renderLanes: Lanes,): boolean {
  // 检查fiber.lanes是否有任务
  const updateLanes = current.lanes;
  if (includesSomeLane(updateLanes, renderLanes)) {
    return true;
  }
  // ... lazy context 逻辑。 
  // ... lazy context还在测试,并没有启用。
  return false;
}

(workInProgress.flags & DidCapture) === NoFlags

没有suspend和error,可以开始bailout

render后判断

render后发现无需更新的情况,尝试bailout,减少子树render

ClassComponent

// 是否需要update,删除非update的代码
function updateClassInstance (){
  //...
  // 是否需要更新
   const shouldUpdate =
    // 是否有ForceUpdate
    checkHasForceUpdateAfterProcessing() ||
    // 检测组件是否要更新
    // 1. 若有shouldComponentUpdate则由其控制。
    // 2. 若是PureComponent,则判断props、state是否equeal。
    // 3. 无shouldComponentUpdate也不是PureComponent则需要更新。
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    )
  //... 随后执行finishClassComponent
  return shouldUpdate
}

// bailout逻辑,删除非bailout的代码
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // ...
  const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
   // 不需要更新,且 没有错误边界
  if (!shouldUpdate && !didCaptureError) {
    // bailout
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // ... 
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

无forceUpdate&组件无需更新时,触发bailout。

组件是否需要更新按照以下逻辑判断

  1. 有shouldComponentUpdate,由它控制
  2. 如果基类是PuerComponent,判断props&state是否equal
  3. 没有sCU和PC,就需要被更新

FunComponent

// 是否需要update,删除非update的代码
function updateClassInstance (){
  //...
  // 是否需要更新
   const shouldUpdate =
    // 是否有ForceUpdate
    checkHasForceUpdateAfterProcessing() ||
    // 检测组件是否要更新
    // 1. 若有shouldComponentUpdate则由其控制。
    // 2. 若是PureComponent,则判断props、state是否equeal。
    // 3. 无shouldComponentUpdate也不是PureComponent则需要更新。
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    )
  //... 随后执行finishClassComponent
  return shouldUpdate
}

// bailout逻辑,删除非bailout的代码
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // ...
  const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
   // 不需要更新,且 没有错误边界
  if (!shouldUpdate && !didCaptureError) {
    // bailout
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // ... 
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

update&组件无需更新时,命中bailout。

example

function A() {
    const [v, setV] = useState(1)
    console.log('A')
    return <div onClick={() => setV(2)}><AA/></div>
}
function AA() {
    console.log('AA')
    return 'AA'
}

第一次:打印A ,AA

第二次:打印A,子组件命中bailout

第三次:啥也不打印,俩人都没变

MemoComponent

分为SimpleMemo 组件和Memo组件

updateSimpleMemoComponent

// SimpleMemoComponent逻辑
function updateSimpleMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  if (current !== null) {
    const prevProps = current.memoizedProps;
    if (
      // props 浅比较equal
      shallowEqual(prevProps, nextProps) &&
      // ref全等
      current.ref === workInProgress.ref &&
      // 用于热重载
      (__DEV__ ? workInProgress.type === current.type : true)
    ) {
      didReceiveUpdate = false;
      // fiber.lanes没有任务
      if (!checkScheduledUpdateOrContext(current, renderLanes)) {
        workInProgress.lanes = current.lanes;
        // bailout执行
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      }
    }
  }
  // 不能bailout,进入FC
  return updateFunctionComponent(current,workInProgress,Component,nextProps,renderLanes);
}

updateMemoComponent

// 已删除与bailout逻辑不相干代码
function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  if (current === null) {
    const type = Component.type;
    // 简单memo组件,非class组件,没有compare,没有默认props,
    if (
      isSimpleFunctionComponent(type) &&
      Component.compare === null &&
      Component.defaultProps === undefined
    ) {
      let resolvedType = type;
      
      // fiber标记为SimpleMemoComponent
      // 此fiber后续,走updateSimpleMemoComponent函数,不在进入此函数。
      workInProgress.tag = SimpleMemoComponent;
      workInProgress.type = resolvedType;
         // ... 创建SimpleMemo组件
      return updateSimpleMemoComponent();
    }
      // ... mount逻辑
    return child;
  }
    
  const currentChild = ((current.child: any): Fiber); 
  // 检测是否有updateLanes任务,逻辑在上面有写。
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
  // 无更新任务
  if (!hasScheduledUpdateOrContext) {
    const prevProps = currentChild.memoizedProps;
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    // caompare对比相同 且 ref全等  
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      // 进行bailout
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // ... 创建newChild
   const newChild = createWorkInProgress(currentChild, nextProps);
   return newChild;
}

ContextComponent

使用了context的组件必定随着context更新,不受性能优化api影响。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  const newValue = newProps.value;

  if (enableLazyContextPropagation) {
    // .. 无逻辑
  } else {
    // oldProps不为null,保证必须传入value 
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      // 新旧value通过Object.is比较。 比全等严格。
      if (is(oldValue, newValue)) {
        if (
          // children全等
          oldProps.children === newProps.children &&
          // 旧版本的context没有变化
          // 旧版本的context不使用了,认为无变化即可。
          !hasLegacyContextChanged()
        ) {
          // bailout
          return bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes);
        }
      } else {
        // 注意:会给所有使用此context的子组件,安排一个lanes任务
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }
    
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

oldProps!==null&is(oldValue,newValue)&oldProps.children===newProps.children时命中bailout。

性能优化应该遵循的步骤

  1. 寻找项目中性能损耗严重的子树
  2. 在子树的根节点使用性能优化api
  3. 子树中采用变与不变分离的策略

总结

  • 组件内state、context的更新都会引起整个子组件rerender(可尝试使用MemoComponent减少render)
  • 减少props属性改变,api及生命周期才能起作用(在子组件中使用它们)
  • ContextProvider会对比children、value,所以子组件尽量用children传递 保证不变

参考

# 如何减少React render次数? 先了解fiber bailout逻辑!

# React内部让人迷惑的性能优化策略