高性能React开发
项目开发到一大半效率都很高,很顺利。但是开发完图标详情页,当渲染上千个图标并展示时,出问题了。页面所有交互都变得卡顿。上图是一个点击弹出下载对话框的示例,大概有半秒延时。
你不知道的Render
其实我们碰到了一个React开发中最普遍的一个性能问题——重复渲染。先讲点基础的,关于React组件的更新。
我们知道React渲染页面是调用组件的render方法进行渲染。我们更新组件是通过setState方法,触发组件重新render,更新页面。上图的例子,我们希望调用根组件的setState改变数据状态,使最终传到绿色组件的数据状态发生改变,使绿色组件得到更新。我们希望的组件调用render方法路径如下绿色组件
实际上会是这样的吗?
实际所有组件都进行了render,然后产生新的虚拟DOM和以前进行diff,然后根据不一样的地方进行统一的DOM操作,更新页面。所以图上黄色组件调用的render,都是重复渲染。那么如何避免这种浪费呢?
shouldComponentUpdate
很简单,利用React自身提供的生命周期函数
shouldComponentUpdate(nextProps, nextState)
他会在组件render之前执行,它返回false
则不再执行组件的render,true
则执行render,默认返回true
。所以默认是不会避免重复渲染的,需要我们自己实现避免逻辑。
好我们来实现避免重复渲染的demo:
我们在每个图标组件IconButton
上实现了避免重复渲染的demo逻辑。看下效果:
现在只有90-100ms 延时,速度提升了80%。但是我们想一想我们还有优化的空间吗?
组件拆分
进一步优化就体现出组件拆分的重要性了。我们刚才shouldComponentUpdate
是在每个IconButton
里实现的,可能有成百上千个IconButton
,我们为了避免重复渲染它每次都要执行成百上千次shouldComponentUpdate
里的demo,这是不是也有点浪费。我们为什么不判断数组有没改变,而不是一个一个判断数组里元素改没改变,来避免重复渲染。所以我们重新划分组件:
如图所示我们把IconButton
划分为IconButtons
的子组件,在IconButtons
上实现避免重复渲染逻辑,只需要判断传入的icons
数组有没有变化。看下效果:
immutable对象
一路优化下来看似挺简单的,实际每次写shouldComponentUpdate
里面的demo逻辑非常麻烦的。上面的示例都简化了的,如何组件添加了其他属性,还需要在shouldComponentUpdate
添加监控其变化的代码逻辑。
所以官方提供了pure-render-mixin
这个包,封装了shouldComponentUpdate
的代码逻辑,方便我们直接使用。但是它有一个缺陷,只对对象第一层属性的变化监控有效,因为它只是实现对nextprops
和nextState
的一个浅比较。因为实现深比较,深拷贝同样是非常耗性能的。
所以当应用比较复杂时,最好使用React优化神器 immutable对象。不具体讲immutable对象API使用,只是讲React使用它的原理,搞清楚它的作用和威力才有使用的冲动嘛。
immutable对象即——不可变对象,每次修改对象的属性都会返回一个新的对象。但这个新的对象要打个引号,它其实是一种共享型的数据结构。如图所示,被修改的属性节点会一直回溯它的父节点,把它们都拷贝成新的值,但不受影响的其他分支内容还是共享以前的。这样有什么好处呢?
当新的props对象传入组件的话,子组件只需要判断引用就可判断是否更新组件。用这种数据结构,我们要判断节点是否要更新,只需要比较传进来的props值或引用就行,比较成本几乎为0。
使用上搭配Flux或Redux使用,在reducer层统一用immutable对象API管理state的修改与更新。Get到那么多优化技能,还有啥性能问题能难道我们?
还会有问题?
竟然提出了,肯定是还有问题要解决。
提供稳定的key
我们发现我的图标项目页面,删除图标时响应非常慢。我们看下了下这个页面的demo实现,用的是一个列表类组件——用数组map返回的一个组件数组。然后在官网上看到列表类组件的使用最好提供稳定的key。
因为React是通过两种方式识别一个组件:
- 组件的类名
- 组件的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过程,相当于以第一次渲染页面。
终于可以上线了
项目开发完成,但是要把它放到生产环境我们还是有一些事情要做的。
- 压缩去掉注释
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false
}
compress: {
warnings: false
}
}),
- 分离样式文件
{
test: /\.less$/,
loader: ExtractTextPlugin.extract('style-loader/useable', 'css-loader?minimize!postcss-loader!less-loader?{"sourceMap":true,"modifyVars":' + JSON.stringify(theme) + '}')
},
- 生产模式打包React
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
},
...
}),
去掉开发模式下属性的验证和警告信息的记录。这样整体性能会得到提升,如果你之前还存在没有解决的性能问题,或许就消失啦。
总结优化tips
-
shouldComponentUpdate
避免重复渲染 - redux + immutable应对大型复杂应用
- 拆分组件尽量让组件更小
- 性能分析工具
react-addons-perf
分析性能瓶颈 - 列表类组件提供稳定的key
- 方法bind一律置于constructor中,避免直接传对象,数组字面量属性
- 慎用对象扩展运算符(...)
- ...