高性能React开发

2017-01-14  本文已影响228人  amnsss
450-500ms 延时

项目开发到一大半效率都很高,很顺利。但是开发完图标详情页,当渲染上千个图标并展示时,出问题了。页面所有交互都变得卡顿。上图是一个点击弹出下载对话框的示例,大概有半秒延时。

你不知道的Render

其实我们碰到了一个React开发中最普遍的一个性能问题——重复渲染。先讲点基础的,关于React组件的更新。

我们知道React渲染页面是调用组件的render方法进行渲染。我们更新组件是通过setState方法,触发组件重新render,更新页面。上图的例子,我们希望调用根组件的setState改变数据状态,使最终传到绿色组件的数据状态发生改变,使绿色组件得到更新。我们希望的组件调用render方法路径如下绿色组件

实际上会是这样的吗?

实际所有组件都进行了render,然后产生新的虚拟DOM和以前进行diff,然后根据不一样的地方进行统一的DOM操作,更新页面。所以图上黄色组件调用的render,都是重复渲染。那么如何避免这种浪费呢?

shouldComponentUpdate

很简单,利用React自身提供的生命周期函数

他会在组件render之前执行,它返回false则不再执行组件的render,true则执行render,默认返回true。所以默认是不会避免重复渲染的,需要我们自己实现避免逻辑。

好我们来实现避免重复渲染的demo:

我们在每个图标组件IconButton上实现了避免重复渲染的demo逻辑。看下效果:

90-100ms 延时

现在只有90-100ms 延时,速度提升了80%。但是我们想一想我们还有优化的空间吗?

组件拆分

进一步优化就体现出组件拆分的重要性了。我们刚才shouldComponentUpdate是在每个IconButton里实现的,可能有成百上千个IconButton,我们为了避免重复渲染它每次都要执行成百上千次shouldComponentUpdate里的demo,这是不是也有点浪费。我们为什么不判断数组有没改变,而不是一个一个判断数组里元素改没改变,来避免重复渲染。所以我们重新划分组件:

如图所示我们把IconButton划分为IconButtons的子组件,在IconButtons上实现避免重复渲染逻辑,只需要判断传入的icons数组有没有变化。看下效果:

20-30ms 延时

immutable对象

一路优化下来看似挺简单的,实际每次写shouldComponentUpdate里面的demo逻辑非常麻烦的。上面的示例都简化了的,如何组件添加了其他属性,还需要在shouldComponentUpdate添加监控其变化的代码逻辑。
所以官方提供了pure-render-mixin这个包,封装了shouldComponentUpdate的代码逻辑,方便我们直接使用。但是它有一个缺陷,只对对象第一层属性的变化监控有效,因为它只是实现对nextpropsnextState的一个浅比较。因为实现深比较,深拷贝同样是非常耗性能的。
所以当应用比较复杂时,最好使用React优化神器 immutable对象。不具体讲immutable对象API使用,只是讲React使用它的原理,搞清楚它的作用和威力才有使用的冲动嘛。

immutable
immutable对象即——不可变对象,每次修改对象的属性都会返回一个新的对象。但这个新的对象要打个引号,它其实是一种共享型的数据结构。如图所示,被修改的属性节点会一直回溯它的父节点,把它们都拷贝成新的值,但不受影响的其他分支内容还是共享以前的。这样有什么好处呢?
当新的props对象传入组件的话,子组件只需要判断引用就可判断是否更新组件。用这种数据结构,我们要判断节点是否要更新,只需要比较传进来的props值或引用就行,比较成本几乎为0。
使用上搭配Flux或Redux使用,在reducer层统一用immutable对象API管理state的修改与更新。Get到那么多优化技能,还有啥性能问题能难道我们?

还会有问题?

竟然提出了,肯定是还有问题要解决。

提供稳定的key

我们发现我的图标项目页面,删除图标时响应非常慢。我们看下了下这个页面的demo实现,用的是一个列表类组件——用数组map返回的一个组件数组。然后在官网上看到列表类组件的使用最好提供稳定的key。
因为React是通过两种方式识别一个组件:

  1. 组件的类名
  2. 组件的key

如果在渲染组件时发现新的虚拟DOM和之前不是相同的组件渲染的,则不用进行虚拟DOM的diff,直接使用新的虚拟DOM渲染页面。所以我们这个页面导致慢的原因是因为使用了index作为key
删除前的icons数组

icons = [
  { id: 1, name: 'xxx'}, // index 0
  { id: 2, name: 'xxx'}, // index 1
  { id: 3, name: 'xxx'}, // index 2
  { id: 4, name: 'xxx'}, // index 3
  ...
  { id: 1000, name: 'xxx'}, // index 999
]

删除第二个图标后

icons = [
  { id: 1, name: 'xxx'}, // index 0
  { id: 3, name: 'xxx'}, // index 1
  { id: 4, name: 'xxx'}, // index 2
  ...
  { id: 1000, name: 'xxx'}, // index 998
]

使用index作为key,React在渲染组件时发现从第二个组件开始每个组件的虚拟DOM和之前比内容都发生了变化,需要进行更新操作,这一更新就是998个组件啊。
如果使用id作为key,React在渲染组件后根据id去找旧的组件的虚拟DOM,由于每个组件的id前后没有发生变化,所以diff不会做大量的更新,React只会发现新的虚拟DOM少量一个id=2的组件,所以只是进行一个删除操作。
同理,列表类组件如果存在排序,增删等操作提供稳定的key,可以减少很多DOM的更新操作。

diff也会慢

在图标库详情页面我们又遇到了一个问题,当用slider批量控制页面的图标大小时,非常卡顿。这次我们姿势都用对了,是什么造成如此卡顿的?


滑动卡顿

我们对整个渲染过程捋了一下,觉得所有图标进行diff更新size的时候,diff过程计算量其实是非常大的,会不会是diff过程导致慢的?我们尝试不经过React渲染,直接操作DOM,相当于跳过渲染虚拟DOM,diff过程,结果很流畅。


滑动顺畅

这里的解决办法不是很优雅,但是提供了一个思考性能问题的方向。有时候我们觉得diff过程是多余的,想直接渲染新的内容,这里还可以提供一种方案。在上面提到列表类组件是根据key来识别组件的,如果我们每次为所有组件提供新的key,是会直接跳过diff过程,相当于以第一次渲染页面。

终于可以上线了

项目开发完成,但是要把它放到生产环境我们还是有一些事情要做的。

  1. 压缩去掉注释
new webpack.optimize.UglifyJsPlugin({
      output: {
          comments: false
      }
      compress: {
        warnings: false
      }
    }),
  1. 分离样式文件
{
        test: /\.less$/,
        loader: ExtractTextPlugin.extract('style-loader/useable', 'css-loader?minimize!postcss-loader!less-loader?{"sourceMap":true,"modifyVars":' + JSON.stringify(theme) + '}')
},
  1. 生产模式打包React
new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      },
      ...
}),

去掉开发模式下属性的验证和警告信息的记录。这样整体性能会得到提升,如果你之前还存在没有解决的性能问题,或许就消失啦。

总结优化tips

上一篇下一篇

猜你喜欢

热点阅读