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

简介: # 前言 用 React 一段时间了,也做了不少列表页。在用 React 做无限下拉加载的列表页时发现个问题:页面前几页渲染速度还挺快的,但是越往下拉加载内容页面的渲染就越慢。这是怎么回事呢? 让我们先来看下 React 的组件渲染流程吧。 # React 的组件渲染流程 React 的组件渲染分为初始化渲染和更新渲染。在初始化时,React 会调用根组件下所有组件的 render 方

前言

用 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(); 
  1. 执行某个操作,如滚动屏幕来加载列表
  2. 然后在控制台里输入如下代码(以 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 也能够帮助我们发现代码中的问题。工具在很大程度上能够给我们带来效率上提升。
但在使用工具的同时,我们也要提高自己代码的质量,合理添加注释,及时清理垃圾代码,优化代码,这样不管是代码执行效率,还是后续的维护都能更高效。

相关文章
|
18天前
|
前端开发 JavaScript
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
本文介绍了在React项目中实现路由懒加载的方法,使用React提供的`lazy`和`Suspense`来优化项目首次加载的速度。通过将路由组件改为懒加载的方式,可以显著减少初始包的大小,从而加快首次加载速度。文章还展示了如何使用`Suspense`组件包裹`Switch`来实现懒加载过程中的fallback效果,并提供了使用前后的加载时间对比,说明了懒加载对性能的提升作用。
58 2
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
|
18天前
|
前端开发 JavaScript UED
react或者vue更改用户所属组,将页面所有数据进行替换(解决问题思路)____一个按钮使得页面所有接口重新请求
在React或Vue中,若需在更改用户所属组后更新页面所有数据但不刷新整个页面,可以通过改变路由出口的key值来实现。在用户切换组成功后,更新key值,这会触发React或Vue重新渲染路由出口下的所有组件,从而请求新的数据。这种方法避免了使用`window.location.reload()`导致的页面闪烁,提供了更流畅的用户体验。
34 1
react或者vue更改用户所属组,将页面所有数据进行替换(解决问题思路)____一个按钮使得页面所有接口重新请求
|
18天前
|
前端开发
React页面跳转取消上一个页面的所有请求
React页面跳转时取消上一个页面的所有axios请求,通过axios拦截器设置cancelToken,并在页面跳转时调用cancel函数取消未完成的请求。
12 2
|
18天前
|
前端开发 JavaScript
React配合axios请求拦截校验session,403跳转至登陆页面
React中使用axios进行请求拦截,通过自定义事件监听和响应拦截实现403状态码时的自动登录页面跳转。
22 2
|
2月前
|
资源调度 前端开发 API
React Suspense与Concurrent Mode:异步渲染的未来
React的Suspense与Concurrent Mode是16.8版后引入的功能,旨在改善用户体验与性能。Suspense组件作为异步边界,允许子组件在数据加载完成前显示占位符,结合React.lazy实现懒加载,优化资源调度。Concurrent Mode则通过并发渲染与智能调度提升应用响应性,支持时间分片和优先级调度,确保即使处理复杂任务时UI仍流畅。二者结合使用,能显著提高应用效率与交互体验,尤其适用于数据驱动的应用场景。
60 20
|
2月前
|
前端开发
React 如何使用条件渲染
【8月更文挑战第17天】React 如何使用条件渲染
35 3
|
2月前
|
前端开发 UED 开发者
React组件优化全攻略:深度解析让你的前端应用飞速运行的秘诀——从PureComponent到React.memo的彻底性能比较
【8月更文挑战第31天】在构建现代Web应用时,性能是提升用户体验的关键因素。React作为主流前端库,其组件优化尤为重要。本文深入探讨了React组件优化策略,包括使用`PureComponent`、`React.memo`及避免不必要的渲染等方法,帮助开发者显著提升应用性能。通过实践案例对比优化前后效果,不仅提高了页面渲染速度,还增强了用户体验。优化React组件是每个开发者必须关注的重点。
55 0
|
2月前
|
前端开发 JavaScript 中间件
|
2月前
|
前端开发 JavaScript 数据管理
React 中无渲染组件
【8月更文挑战第31天】
23 0
|
2月前
|
前端开发 JavaScript 搜索推荐