React学习笔记——性能优化篇

452 阅读5分钟

实习阶段有幸参与了团队内部平台的建设,某天在进行页面的迭代更新时发现页面有太多次没意义的render,众所周知,重排是非常浪费页面性能的,所以应该尽可能避免这种情况,所以用了一下午的时间对平台页面进行了优化,并写下这篇文章来记录一下学习过程。

react给了开发者极大的自由,只提供了核心的API,但是自由的同时也伴随着代码混乱,虽然react提供了强大的虚拟dom来帮助我们提高web性能,但是谁不想自己的页面加载的更快呢,我主要是使用了生命周期函数shouldComponentUpdate来进行性能优化:

1、浅比较避免重复渲染

当一个组件的props或者state发生了改的时候,react会借助vdom对比前后变化,当dom发生变化时,react会更新dom,当父组件重新渲染时,伴随着子组件也会重新渲染,试想一下,如果一个组件内部有1,000个子组件,那会浪费多少性能。所以在一些情况下,你可以通过shouldComponentUpdate这个生命周期函数来阻止react更新dom,减少render的次数。这个函数在重新渲染之前触发,默认返回true,当返回false时,会阻止这次渲染。引用官网的一张图来进一步说明

SCU表示shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)

根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

C1根节点,绿色SCU、红色vDOMEq,表示需要更新。

C2节点,红色SCU,表示不需要更新,同时C4、C5作为其子节点也不需要检查更新。

C3节点,绿色SCU、红色vDOMEq,表示需要更新。

C6节点,绿色SCU、红色vDOMEq,表示需要更新。

C7节点,红色SCU,表示不需要更新。

C8节点,绿色SCU,表示React需要渲染这个组件;绿色vDOMEq,表示虚拟DOM一致,不更新DOM。

因此,我们可以根据我们组件的实际情况,在真正需要重新渲染的时候再返回true,其他时候都返回false。shouldComponentUpdate有两个参数nextState,nextProps,一般情况下我们是根据这两个值的变化情况选择返回true还是false。像下面这段代码。

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在以上代码中,shouldComponentUpdate只检查props.color和state.count的变化。如果这些值没有变化,组件就不会更新。当你的组件变得更加复杂时,你可以使用类似的模式来做一个“浅比较”,用来比较属性和值以判定是否需要更新组件。这种模式十分常见,因此React提供了一个辅助对象来实现这个逻辑 - 继承自React.PureComponent。

大部分情况下,你可以使用React.PureComponent而不必写你自己的shouldComponentUpdate,但是它只做一个浅比较,当你比较的目标为引用类型数据,变量引用地址是没有改变的,引用数据发生了改变,浅比较就显得优点乏力了。

2、使用不可突变数据避免重复渲染

引用官网中的例子解释一下突变数据产生的问题。例如,假设你想要一个ListOfWords组件来渲染一个逗号分隔的单词列表,并使用一个带了点击按钮名字叫WordAdder的父组件来给子列表添加一个单词。以下代码并不正确:

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() {
    // 这段内容将会导致代码不会按照你预期的结果运行
    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>
    );
  }
}

导致代码无法正常工作的原因是 PureComponent 仅仅对 this.props.words的新旧值进行“浅比较”。在words值在handleClick中被修改之后,即使有新的单词被添加到数组中,但是this.props.words的新旧值在进行比较时是一样的(引用对象比较),因此 ListOfWords 一直不会发生渲染。 避免此类问题最简单的方式是避免使用值可能会突变的属性或状态,如:

数组使用concat

handleClick() {
  this.setState(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}

对象使用Object.assign()

handleClick() {
// 假设我们有一个叫colormap的对象,下面方法不污染原始对象
  this.setState(prevState => ({
    words: Object.assign({}, colormap, {right: 'blue'})
  }));
}

数组和对象都可以使用的JSON.parse(JSON.stringify(something))和解构赋值

handleClick() {
  this.setState(prevState => ({
    words: JSON.parse(JSON.stringify(prevState))
  }));
}
handleClick() {
  this.setState(prevState => ({
    words: [...prevState.words, 'marklar'],
  }));
};

或者使用不可突变数据immutable.js

immutable.js使得变化跟踪很方便。每个变化都会导致产生一个新的对象,因此我们只需要检查索引对象是否发生改变。

const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
x === y; // false

在这个例子中,x突变后返回了一个新的索引,因此我们可以安全的确认x被改变了。 不可突变的数据结构帮助我们轻松的追踪对象变化,从而可以快速的实现shouldComponentUpdate。

推荐一个react性能检测工具

react16版本之前,我们可以使用react-addons-perf工具来查看,而在最新的16版本,我们只需要在url后加上?react_pref。 首先来了解一下react-addons-perf。 react-addons-perf这是 React 官方推出的一个性能工具包,可以打印出组件渲染的时间、次数、浪费时间等。 简单说几个api,具体用法可参考官网

Perf.start() 开始记录

Perf.stop() 结束记录

Perf.printInclusive() 查看所有设计到的组件render

Perf.printWasted() 查看不需要的浪费组件render

再来了解一下,react16版本的方法,在url后加上?react_pref,就可以在chrome浏览器的performance中查看了。