React组件的性能优化

1,669 阅读6分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

虚拟DOM 很好,但别贪杯。

React对页面更新都是对组件的重新渲染,将之前渲染的内容全部再渲染一次。而是利用虚拟DOM来达到最小化修改真实DOM,达到高效的更新。

但是,有时明确知道渲染结果不会有变化,那么这时对虚拟DOM的计算就是“一次浪费”。

所以如果能够在开始计算虚拟DOM之前就可以判断,那样可以干脆不要进行虚拟DOM计算和比较,速度就会更快。

注意,这里说的浪费是计算Virtual DOM的浪费,并不是访问DOM树的浪费。

单个 React 组件的性能优化

shouldComponentUpdate

作为一个生命周期函数,shouldComponentUpdate 函数则决定“什么时候组件不需要重新渲染”。

这个函数默认返回是true,也就是默认每次更新的时候都要调用所有的生命周期函数。包括调用render函数,根据render函数的返回结果计算虚拟DOM。

当返回false时,就不会进行组件的更新。我们需要根据自身考虑自定义改写返回false的情况。

<App style={{color:'red'}} onClick={() => sayHello()} />

shouldComponentUpdate函数实现,每一次渲染都会认为style这个prop发生了变化,因为每次都会产生一个新的对象给style,在“浅层比较”中,类似== 先判断对象的指针,这里创建了新对象{color:'red'},即使里面内容一样、也会认为是传入了一个新对象,即新props。那么App组件会重新渲染。

这里赋值给onClick的是一个匿名的函数,而且是在赋值的时候产生的。也就是说,每次渲染App组件的时候,都会产生一个新的函数,也会造成App组件会重新渲染。

如果需要做“深层比较”,那就是某个特定组件的行为,需要开发者自己根据组件情况去编写。不过也要谨记,不要简单地递归比较所有层次的字段,因为传递进来的prop对象什么结构是无法预料的。

这里官网有具体实现方式

shouldComponentUpdate 的作用

React.PureComponent 、 React.memo做“浅层比较”

大部分情况下,可以使用 React.PureComponent来代替手写 shouldComponentUpdate

但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。

例如当state是一个对象时,传入给子组件,即便state内部属性值改变。子组件也依然会认为props并未改变而不会更新。这也就是为什么React推荐使用不可变数据

这里引用官网的例子

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug 
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

解决方法有很多,但是核心一点就是,不要直接修改原来的state而是要复制一个新的state在新的state的基础上进行改进。

官网的解决方案

多个React组件的性能优化

多个React组件之间组合的渲染过程。和单个React组件的生命周期一样,React组件也要考虑三个阶段:装载阶段、更新阶段和卸载阶段。

装载阶段和卸载阶段都是组件必须经历一次的,这无可避免,也没有什么太多可以考虑的点。 那么我们主要看更新阶段。

React的调和(Reconciliation)过程

在装载过程中,React通过render方法在内存中产生了一个树形的结构,树上每一个节点代表一个React组件或者原生的DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个Virtual DOM来渲染产生浏览器中的DOM树。

Reconciliation(调和)就是React找寻新旧虚拟DOM的不同的过程。

按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是O(N3),但是为了降低算法时间复杂度,React做出了部分割舍。

1.节点类型不同的情况

如果树形结构根节点类型不相同,重新构建新的DOM树,原有的树形上的React组件会经历“卸载”的生命周期,即便可能这颗树的某些节点可以复用,为了避免O(N3)的时间复杂度,React必须要选择一个更简单更快捷的算法,也就只能采用这种方式。

这里旧组件会经历卸载过程,新组件会经历装载过程。

2.节点类型相同的情况

如果两个树形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。

根据组件类型进行不同处理

  1. DOM元素组件

对于DOM元素类型,React会保留节点对应的DOM元素,只对树形结构根节点上的属性和内容做一下比对,然后只更新修改的部分。

  1. React组件

React能做的只是根据新节点的props去更新原来根节点的组件实例,引发这个组件实例的更新过程。

在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。

3.多个子组件的情况

找出两个子组件序列的不同之处,现有的计算出两个序列差异的算法时间是O(N2)。同样为了降低时间复杂度,React选择了不是寻找两个序列的精确差别,而是直接挨个比较每个子组件。

当新添加子组件不是在末尾插入时,子组件之间的顺序被打乱了,就会造成旧的子组件不能被复用的处境(只能触发旧组件的更新)。

这里React又推出了一个Key关键属性来解决这个问题

key

React官网的key

key属性就像身份证一样,每个组件都可以拥有一个独一无二的key属性用于标识。

当新添加子组件不是在末尾插入时,子组件之间的顺序被打乱了,但是我们有key属性,所以我们就可以靠key属性来尽可能复用旧子组件(避免旧组件的更新)。

注意:

key值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导React做出错误判断,甚至导致错误的渲染结果。也就是要保证key的值在组件的装载->更新->卸载的过程都得保证一致。

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs  <li key={index}>    {todo.text}
  </li>
);

如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响这一篇文章。翻译版

如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

参考

《深入浅出React和Redux》 《React官网》