React中的性能优化-更新阶段优化

作者: 贺鹏飞 分类: React,数据可视化 发布时间: 2021-04-17 13:55

前言

React中的性能优化日常开发工作中不可缺少的,他包括几个方面,编译阶段的优化、组件更新阶段的优化、Redux等状态管理器的性能优化、大数据渲染时的性能优化、一些性能分析工具的使用和性能分析等等。今天我们就说说关于组件更新阶段的一些优化的点。

一、shouldComponentUpdate

组件更新生命周期中的shouldComponentUpdate(SCU)从字面上来理解它问的是是否需要进行更新,默认返回的true进行更新。但在组件渲染的过程中,有时候并没有用到props/state或者是在父组件重新渲染时子组件的props/state并没有发生改变,这时render得到的是和之前一样的虚拟DOM,所以我们要使用SCU来进行组件是否需要更新的判断。通过这个API我们可以拿到改变前后的props/state,手动的检查状态是否发生了更新,再根据实际的变量情况决定是否需要进行重新渲染。

在React里,shouldComponentUpdate源码为:

if (this.compositeType === CompositeTypes.PureClass) { 
     shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState); 
} 

shouldComponentUpdate使用方法:

shouldComponentUpdate(nextProps, nextState)

使用shouldComponentUpdate()以让React知道当前状态或属性的改变是否不影响组件的输出,默认返回ture,返回false时不会重写render,而且该方法并不会在初始化渲染或当使用forceUpdate()时被调用,我们要做的只是这样:

shouldComponentUpdate(nextProps, nextState) {
		if(nextProps.content !== this.props.content) {
			  return true;
		}else {
		  	return false;
		}
}

二、PureComponent

使用PureComponent会帮你内置一个ShouldComponentUpdate的生命周期,ShouldComponentUpdate生命周期函数内部通过props和state的浅比较来决定是否需要渲染,如果之前的prevProp和prevState跟当前的props,state浅比较相同的话,就会返回false,组件就不会进行渲染。

React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则可能无法检查到深层的差别,产生错误的对比结果。所以只在props 和 state 比较简单时使用这个,或者在深层数据结构发生变化时调用forceUpdate()来确保组件被正确地更新。也可以使用immutable 对象加速嵌套数据的比较。

class Child extends React.PureComponent {
    render(){
        console.log('I am rendering');
        return (
            <div>I am update every {this.props.seconds} seconds</div>
        )
    }
}

三、immutable.js

上面说在使用PureComponent时,它只是进行了浅层比较,如果props传入的对象嵌套的层级太多,可能会引起props或者是state引用地址未发生变化从而导致shouldComponentUpdate返回false,未触发render函数,没有渲染组件的情况,这时我们就可以用到immutable.js库来进行优化。immutable.js是一个持久性数据结构的库。

Immutable Date 是一旦创建就不能再被更改的数据,对immutable对象的任何修改或添加删除操作都会返回一个新的immutable对象。它在使用旧数据创建新数据时,要保证旧数据可用且不变,还要避免deepCopy深拷贝带来的性能上的大量损耗,所以immutable使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点进行共享。Immutable.js常用的API有以下几个is()、Map()、List()等等,下面来介绍几个常用的API is(map1,map2): 对map1和map2进行比较。和js中对象的比较不同,在js中比较两个对象比较的是地址,但是在immutable中比较的是这个对象hashCode和valueOf,只要两个对象的hashCode相等,值就是相同的,避免了深度遍历,提高了性能。

项目中的应用:

<!--删除一个server同时更新store进行页面更新-->
[DELETE_SERVER]: (app, action) => {
    let servers = app.get('servers');  //获取store中的servers
    let ser = action.payload;  //后台返回的结果
    let index = servers.findIndex(i => {
        return i.get('_id') === ser._id;  //判断是否存在
    });
    if (index === -1) {
        return app;   
    } else {
        return app.set('servers', servers.delete(index));  //重新设置删除index后的servers
    }
}
<!--更新一个package同时更新store进行页面更新-->
[UPDATE_PACKAGE]: (app, action) => {
    let packages = app.get('packages');
    let pac = fromJS(action.payload);
    let index = packages.findIndex(i => {
        return i.get('_id') === pac.get('_id')
    })
    if (index === -1) return app;
    app = app.set('package', pac);
    return app.set('packages', packages.update(index, () => {
        return pac
    }));
},

四、React.memo

上面是类组件的优化,那函数组件呢?函数组件采用React.memo()进行优化提高组件性能

React v16.6.0出了一些新的包装函数(wrapped functions),一种用于函数组件PureComponent / shouldComponentUpdate形式的React.memo(),React.memo()是一个高阶函数,它与 React.PureComponent类似,但是一个函数组件而非一个类。

React.memo是一个高阶组件,它仅检查props的变化,如果组件在相同的props的情况下,那可以通过将组件包裹在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React将跳过渲染组件的操作并直接复用最近一次渲染的结果。在默认情况下它和PureComponent一样都是进行浅比较的。

import React from "react";
function Child({seconds}){
    console.log('I am rendering');
    return (
        <div>I am update every {seconds} seconds</div>
    )
};
function areEqual(prevProps, nextProps) {
    if(prevProps.seconds===nextProps.seconds){
        return true
    }else {
        return false
    }
}
export default React.memo(Child,areEqual)

React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新,与shouldComponentUpdate()功能类似。

React.memo原理:其实react.memo的实现很简单,如下:

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  if (__DEV__) {
    if (!isValidElementType(type)) {
      warningWithoutStack(
        false,
        'memo: The first argument must be a component. Instead ' +
          'received: %s',
        type === null ? 'null' : typeof type,
      );
    }
  }
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}

可以看到,最终返回的是一个对象,这个对象带有一些标志属性,在react Fiber的过程中会做相应的处理。

五、useMemo、useCallback

如果传给子组件的派生状态或函数,每次都是新的引用,那么 PureComponent 和 React.memo 优化就会失效。所以需要使用 useMemo 和 useCallback 来生成稳定值,并结合 PureComponent 或 React.memo 避免子组件重新 Render。

useCallback 是「useMemo 的返回值为函数」时的特殊情况,是 React 提供的便捷方式。在 React Server Hooks 代码中,useCallback 就是基于 useMemo 实现的。尽管 React Client Hooks 没有使用同一份代码,但 useCallback的代码逻辑和 useMemo的代码逻辑仍是一样的。

useMemo

useMemo 用于性能优化,通过记忆值来避免在每个渲染上执⾏高开销的计算。

const memoizedValue =useMemo(callback,array)

返回一个 memoized 值。

  • callback是一个函数用于处理逻辑;
  • array 控制useMemo重新执⾏行的数组,array改变时才会 重新执行useMemo;
  • 不传数组,每次更新都会重新计算;
  • 空数组,只会计算一次;
  • 依赖对应的值,当对应的值发生变化时,才会重新计算(可以依赖另外一个 useMemo 返回的值);
  • useMemo的返回值是一个记忆值,是callback的返回值;

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行(切记不是渲染后执行哦)。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

useMemo用法,例子:

function App () {
  const [ count, setCount ] = useState(0)
  const add = useMemo(() => count + 1, [count])
  return (
    <div>
      点击次数: { count }
      <br/>
      次数加一: { add }
      <button onClick={() => { setCount(count + 1)}}>点我</button>
    </div>
    )
}

useCallback

useCallback 可以说是 useMemo 的语法糖,能用 useCallback 实现的,都可以使用 useMemo, 常用于react的性能优化。在 react 中我们经常面临一个子组件渲染优化的问题,尤其是在向子组件传递函数props时,每次 render 都会创建新函数,导致子组件不必要的渲染,浪费性能,这个时候,就是 useCallback 的用武之地了,useCallback 可以保证,无论 render 多少次,我们的函数都是同一个函数,减小不断创建的开销。

const memoCallback= useCallback(callback,array)

返回一个 memoized 回调函数。

  • callback是一个函数用于处理逻辑;
  • array 控制useCallback重新执⾏的数组,array改变时才会重新执⾏useCallback;
  • 不传数组,每次更新都会重新计算;
  • 空数组,只会计算一次;
  • 依赖对应的值,对应的值发生变化重新计算;
  • useCallback返回值是callback本身(useMemo返回的是callback函数的返回值)。
function App () {
  const [ count, setCount ] = useState(0)
  const add = useCallback(() => count + 1, [count])
  return (
    <div>
      点击次数: { count }
      <br/>
      次数加一: { add() }
      <button onClick={() => { setCount(count + 1)}}>点我</button>
    </div>
    )
}

六、列表项使用 key 属性

当渲染列表项时,如果不给组件设置不相等的属性 key,就会收到如下报警。相信很多开发者已经见过该报警成百上千次了,那 key 属性到底在优化了什么呢?举个 在不使用 key 时,组件两次 Render 的结果如下。

<!-- 前一次 Render 结果 -->
<ul>
  <li>bbb</li>
  <li>ccc</li>
</ul>

<!-- 新的 Render 结果 -->
<ul>
  <li>aaa</li>
  <li>bbb</li>
  <li>ccc</li>
</ul>

此时 React 的 Diff 算法会按照 <li> 出现的先后顺序进行比较,得出结果为需要更新前两个<li>并创建内容为 ccc 的li,一共会执行两次 DOM 更新、一次 DOM 创建。如果加上 React 的 key 属性,两次 Render 结果如下。

<!-- 前一次 Render 结果 -->
<ul>
  <li key="002">bbb</li>
  <li key="003">ccc</li>
</ul>

<!-- 新的 Render 结果 -->
<ul>
  <li key="001">aaa</li>
  <li key="002">bbb</li>
  <li key="003">ccc</li>
</ul>

React Diff 算法会把 key 值为 002 的虚拟 DOM 进行比较,发现 key 为 002 的虚拟 DOM 没有发生修改,不用更新。

同样,key 值为 003 的虚拟 DOM 也不需要更新。结果就只需要创建 key 值为 001 的虚拟 DOM。

相比于不使用 key 的代码,使用 key 节省了两次 DOM 更新操作。如果把例子中的 <li> 换成自定义组件,并且自定义组件使用了 PureComponent 或 React.memo 优化。

那么使用 key 属性就不只节省了 DOM 更新,还避免了组件的 Render 过程。

React 官方推荐将每项数据的 ID 作为组件的 key,以达到上述的优化目的。

并且不推荐使用每项的索引作为 key,因为传索引作为 key 时,就会退化为不使用 key 时的代码。

七、批量更新 setState,减少 Render 次数

批量更新 setState 时,多次执行 setState 只会触发一次 Render 过程。相反在立即更新 setState 时,每次 setState 都会触发一次 Render 过程,就存在性能影响。

假设有如下组件代码,该组件在 getData() 的 API 请求结果返回后,分别更新了两个 State 。线上代码实操参考:batchUpdates 批量更新。

function NormalComponent() {
  const [list, setList] = useState(null)
  const [info, setInfo] = useState(null)

  useEffect(() => {
    ;(async () => {
      const data = await getData()
      setList(data.list)
      setInfo(data.info)
    })()
  }, [])

  return (
    <div>
      非批量更新组件时 Render 次数:
      {renderOnce('normal')}
    </div>
  )
}

该组件会在 setList(data.list) 后触发组件的 Render 过程,然后在 setInfo(data.info) 后再次触发 Render 过程,造成性能损失。遇到该问题,开发者有两种实现批量更新的方式来解决该问题:

  1. 将多个 State 合并为单个 State。例如 useState({ list: null, info: null }) 替代 list 和 info 两个 State。
  2. 使用 React 官方提供的 unstable_batchedUpdates 方法,将多次 setState 封装到 unstable_batchedUpdates 回调中。修改后代码如下。
function BatchedComponent() {
  const [list, setList] = useState(null)
  const [info, setInfo] = useState(null)

  useEffect(() => {
    ;(async () => {
      const data = await getData()
      unstable_batchedUpdates(() => {
        setList(data.list)
        setInfo(data.info)
      })
    })()
  }, [])

  return (
    <div>
      批量更新组件时 Render 次数:
      {renderOnce('batched')}
    </div>
  )
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注