如何利用工具提高 React 页面渲染性能

阅读 1558
收藏 39
2016-12-29
原文链接:click.aliyun.com

前言

用 React 一段时间了,也做了不少列表页。在用 React 做无限下拉加载的列表页时发现个问题:页面前几页渲染速度还挺快的,但是越往下拉加载内容页面的渲染就越慢。这是怎么回事呢?
让我们先来看下 React 的组件渲染流程吧。

React 的组件渲染流程

React 的组件渲染分为初始化渲染和更新渲染。在初始化时,React 会调用根组件下所有组件的 render 方法进行渲染。

在每个生命周期更新时,React 会先调用 shouldComponentUpdate(nextProps, nextState) 方法来判断该组件是否需要更新。该方法会返回 true 或 false 来表示更新或不需要更新。如果不需要更新,则直接保持不变;如果需要更新,则调用 render 方法生成新的虚拟 DOM,然后再用 diff 算法与旧的虚拟 DOM 进行对比,如果结果一致就不更新;如果对比不同,则根据最小粒度改变去更新 DOM。
整个过程如下图所示。
_2016_09_22_10_40_05

ShouldComponentUpdate 在默认情况下返回的是 true。也就是说 React 默认会调用所有组件的 render 方法生成虚拟 DOM,然后再与旧虚拟 DOM 比较以确定最终组件是否需要更新。这个 render 和 diff 对比的过程对于只是兄弟组件发生了改变,而本身并没有变化的组件来说,很明显存在资源浪费。

那么如何能直观的知道这些浪费都发生在哪些过程中呢?这就轮到 Perf 出场了。

接下来让我们先来了解下什么是 Perf,再看看它都能做些什么。

什么是 Perf ,它能做些什么?

Perf 是 react 官方提供的性能分析工具,可以对我们的应用进行整体性能分析并提供性能数据。
直接来看下具体有哪些 API 吧:

  • Perf.start():开始测量。
  • Perf.stop():停止测量。
  • Perf.getLastMeasurements():在停止测量之后调用,用来获取 measurements。

接下来就可以打印出性能数据了:

  • Perf.printInclusive(measurements):打印出所花费的整体时间。
  • Perf.printExclusive(measurements):打印出处理 props、getInitialState、调用 componentWillMount 和 componentDidMount 等的时间,这里面不包含 mount 组件的时间。
  • Perf.printWasted(measurements):打印出测量时段内所浪费的时间。这部分信息是分析数据中最有用的一部分了。我们可以通过这个数据找出时间被浪费在了哪儿。浪费一般出现在组件没有渲染任何东西的时候,如上文中提到的,组件在 render 出新的虚拟 DOM 和旧的虚拟 DOM 对比之后,发现不需要更新组件。最理想的情况这个的返回值是一个空数组。
  • Perf.printOperations(measurements):打印出分析时段内发生的底层 DOM 操作。

目前不少 React 性能优化的文档里都有提到可以通过 shouldComponenentUpdate 和 Perf 来进行优化,但是却没有进行详细的说明。一开始的时候我是困惑的:

  • Perf 是怎么跑起来的?
  • 在什么时候执行比较好?
  • 性能报表中的各个指标是什么意思呢?
  • 怎么结合这些数据来进行优化?

翻了不少文档并实践之后,以我们到家业务的店铺列表组件的优化为例,总结出来了以下的使用步骤,仅供大家参考。

Perf 怎么用?

使用步骤:

步骤一:获取

  • 先在页面把原来的 react.js 替换成带组件的版本 react-with-addons.js
    • 这里要补充说明下关于使用的 react-with-addons 的版本
    • 推荐使用最新版本 15.3.2。
    • 如果使用 15.1.0 版本,react-with-addons 有可能会出现 Warning: There is an internal error in the React performance measurement code. We did not expect componentWillMount timer to stop while no timer is still in progress for another instance. Please report this as a bug in React。另外会出现有时候执行 React.addons.Perf.printOperations(measurements); 打印不出信息来等一些奇怪的问题。
    • Perf 是在 0.11.0 版本 中新增的, 然后在 [15.1.0 版本]中进行了重构,并在后续版本中修复了不少 bug,目前还在逐渐完善的过程中。另外要注意的是:在非生产环境是不能使用 Perf 的。

步骤二:调用

方式一:直接在浏览器里调用

  1. 在浏览器的控制台里输入: React.addons.Perf.start();
  2. 执行某个操作,如滚动屏幕来加载列表
  3. 然后在控制台里输入如下代码(以 printWasted 为例): React.addons.Perf.stop(); var measurements = React.addons.Perf.getLastMeasurements(); React.addons.Perf.printWasted(measurements);

这样就能够看到打印出来这一过程所浪费的时间了。
_2016_09_22_8_34_02

方式二:添加到组件代码中

在组件的 componentDidUpdate 方法中调用,这样可以在组件每次发生更新时打印出各个性能数据。

componentWillMount() {
  React.addons.Perf.start();
  // Your code
}
componentDidUpdate() {
  // Your code.
  let Perf = React.addons.Perf;
  Perf.stop();

  let measurements = React.addons.Perf.getLastMeasurements();
  if (measurements.length > 0) {
    Perf.printInclusive(measurements);
    Perf.printExclusive(measurements);
    Perf.printWasted(measurements);
    Perf.printOperations(measurements);

    Perf.start(); // clears measurements and try it again
  }
}

这样就可以在页面连续滚动时打印出多个数据。

接下来让我们看下在这些数据中可以发现什么。

数据指标分析

店铺列表在每次下拉刷新时,先变更列表加载状态,再渲染出列表内容。以从第11页下拉翻到第12页为例,我们先来看下优化前后的效果对比图,如下:
优化前:

  • Perf.printInclusive(measurements)
    _2016_09_22_8_49_29
    _2016_09_22_8_49_53

  • Perf.printExclusive(measurements);
    _2016_09_22_8_50_12
    _2016_09_22_8_50_28

  • Perf.printWasted(measurements)
    _2016_09_22_8_50_47
    _2016_09_22_8_50_57

优化后:

  • Perf.printInclusive(measurements)
    _2016_09_22_8_54_32
    _2016_09_22_8_54_44
    _2016_09_22_8_54_59

  • Perf.printExclusive(measurements);
    _2016_09_22_8_55_19
    _2016_09_22_8_55_28
    _2016_09_22_8_55_42

  • Perf.printWasted(measurements)
    _2016_09_22_8_55_56
    _2016_09_22_8_56_06
    _2016_09_22_8_57_49

从上述图表中可以看到,优化之后整体的渲染时间较时间有较大减少,且浪费时间的时间也大幅减少,在执行过程中,有个生命周期中的浪费时间已经减为0了。

下面就来看看这个优化是怎么做的吧。

优化方案

拆分组件,结合 shouldComponentUpdate,以减少重绘次数。

  • 对于静态组件,shouldComponentUpdate 返回 false;
  • 对于组件存在变化的情况
    • 如果变化的 props 或 state 不多,且层次不深,则可以在 shouldComponentUpdate(nextProps, nextState) 里比较新老 props 和 state,在目标 props 或 state 发生变化时 return ture,其余情况都 return false。
    • 如果变化的 props 和 state 多,或者层次深,则最好把组件拆分成变化的和不变化的部分。

注意:这里必须要先确保组件是静态的,即在 componentDidMount 后不会有任何变化,否则不能直接 return false。
在店铺列表组件优化的过程中,一开始没有留意到 ShopCard 组件中的优惠区域高度是会根据优惠条数的不同而有所不同的,并且具有收起和展开的功能,直接 return false 后导致这个区块撑开的高度有问题了,并且收起/展开的功能也失效了。

  • 改出问题的样子:

_2016_09_10_4_37_43

  • 正常情况初始时的样子:

_2016_09_10_4_38_10

  • 正常情况展开后的样子:

_2016_09_10_4_38_18

就拿 ShopCard 组件的代码作为例子看下 shouldComponentUpdate 是怎么样的吧:

  shouldComponentUpdate(nextProps, nextState) {
    let { shouldShowMoreActivities, height } = this.state;

    return shouldShowMoreActivities && height !== nextState.height;
  }

因为这个组件只会受是否有优惠活动和优惠撑开后的高度所影响,所以只要关注 shouldShowMoreActivities 和 height 这两个 state 即可。

修改后整体效果如下:

  • Perf.printInclusive(measurements)
    _2016_09_22_11_17_07
    _2016_09_22_11_17_20

  • Perf.printExclusive(measurements);
    _2016_09_22_11_17_34
    _2016_09_22_11_17_46

  • Perf.printWasted(measurements)
    _2016_09_22_11_18_01
    _2016_09_22_11_18_17

从优化后的效果图中可以看到 ShopCard 组件只渲染了最后一页增加的7项,另外,render time、render count 都从原来的上百减至几个了,且浪费的时间也从原来的几十毫秒减为个位数了。效果还是比较明显的。
但是如果每个组件都要手动覆盖 shouldComponentUpdate 方法也是比较费时的事情,并且这个方法的重写也需要谨慎,可能会带来意想不到的问题。
接下来让我们看下 React 有没有为这个事情做点什么吧。

PureRenderMixin

如果你的组件在相同输入的时候都能够有相同的产出,那么就可以使用 React 提供的 PureRenderMixin 插件,它会自行为组件绑定 shouldComponentUpdate 方法,对现有的子组件的 state 和 props 进行判断。但是它只支持基本类型的浅度比较,如果组件的 props 和 state 数据结构层次复杂则不适用。使用方法如下:

class Shop extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = React.addons.PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  render() {
    // Your code
  }
}

说明:如果页面上引入的是 react.js,可以自行安装 react-addons-pure-render-mixin 依赖后以如下方式引入:

import PureRenderMixin from 'react-addons-pure-render-mixin';

效果如下图(以 Perf.printWasted(measurements) 为例):
_2016_09_22_11_29_10
_2016_09_22_11_29_19

相对于最初版本的已经少了很多,不过比自己实现 shouldComponentUpdate 还是多浪费了 ShopCard 的 15 次 render。

React.PureComponent

在 react 的最新版本里面,还提供了 React.PureComponent 的基础类,直接把原来的 React.Component 替换成 React.PureComponent 即可。
效果如下图(以 Perf.printWasted(measurements)
为例):
_2016_09_22_11_42_18
_2016_09_22_11_42_29

效果和使用 PureRenderMixin 差不多。只是需要注意的是 PureComponent 是在 15.3.0 版本中才开始支持的。

另外,Facebook 还提供了一个专门处理不可变数据的库 immutable.js ,大家感兴趣的可自行了解。

清理组件之间不关联的 props 映射

当父组件包含多个子组件,子组件之间存在交互的情况下,有些场景里父组件只是受子组件的某一个属性影响,或者一个子组件只受另外子组件的某些属性影响,那么在 mapStateToProps 的时候就要在各自的 Container 里面把受影响组件的那几个相关 state 映射到 props 里。
但是组件一多,属性一多,这就是件很费神的事情,尤其是写的过程中发现要增加 state 了,就要在关联组件的 mapStateToProps 中挨个加一遍,有时候发现某个属性用不到了又要挨个删一遍。不知道大家有没有这种体验,还是我的使用姿势不对?反正每当这种时候我就特别想把要用到的状态所属的组件定义的整个 state 对象塞到自己的 props 里,这样不管后面加多少 state,也不用再加一遍,而是直接拿这个对象的属性就好了。
但是,这样会有一个副作用,某组件只要一个属性更新了,映射了该组件所属 state 到自己的 props 里的组件就会触发重新渲染了。而如上所说,shouldComponentUpdate 和 PureComponent 适用场景有限。因此,在代码层面能做的优化还是直接做掉吧,而且梳理一遍 props 和 state,可以对组件之间的交互逻辑更了解。
在简化了 props 后,自己编写 shouldComponentUpdate 也会简单很多。

效果如下图(以 Perf.printWasted(measurements) 为例):
_2016_09_22_11_48_21

从结果中可以看到少了很多浪费时间的项目。

React 页面的性能优化方案还有很多,如合并 setState,合并 dispatch,渐进式渲染等,key,这里就先不一一展开了,后续再讲。

小结

本文主要讲述了如何使用 Perf 性能分析工具结合 React 提供的 shouldComponentUpdate 方法、PureRenderMixin 插件 和 PureComponent 组件来提高 React 组件的渲染性能。
还有其他很多工具如 Chrome 的 Timeline 和 Profiles 也能够帮助我们发现代码中的问题。工具在很大程度上能够给我们带来效率上提升。
但在使用工具的同时,我们也要提高自己代码的质量,合理添加注释,及时清理垃圾代码,优化代码,这样不管是代码执行效率,还是后续的维护都能更高效。

评论