写了一段时间的
react
之后,渐渐的喜欢上了使用react
来写应用。我们知道,
react
时打出的旗号之一就是高性能。今天我们还一起来聊一聊
react
的性能优化,思考还能通过哪些手段来提升React的性能,使我们的react
更快,性能更好。
一,react组件的性能优化(渲染角度优化)
1,react性能查看工具
再讲性能优化之前,我们需要先来了解一下如何查看react加载组件时所耗费的时间的工具,在react 16版本之前我们可以使用React Perf
来查看。
大家可以在chorme中先安装React Perf扩展,然后在入口文件或者redux
的store.js
中加入相应的代码即可:
在最新的React16版本中,我们可以直接在url后加上?react_pref
,就可以在chrome浏览器的performance
,我们可以查看User Timeing
来查看组件的加载时间。
使用此工具的具体操作大家可以看下图:
2,单个react组件性能优化
2.1,render
里面尽量减少新建变量和bind
函数,传递参数是尽量减少传递参数的数量。
首先我们先思考一个问题,比如我要实现一个点击按钮使相应的num
增加1,我们有哪一些方法。
大家应该都能想到,无非就是三种,如下图:
第一种是在构造函数中绑定this
,第二种是在render()
函数里面绑定this
,第三种就是使用箭头函数,都能实现上述方法;
但是哪一种方法的性能最好,是我们要考虑的问题。应该大家都知道答案:第一种的性能最好。
因为第一种,构造函数每一次渲染的时候只会执行一遍;
而第二种方法,在每次render()
的时候都会重新执行一遍函数;
第三种方法的话,每一次render()
的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。
第三种方法我们可以举一个例子,因为react
判断是否需要进行render
是浅层比较,简单来说就是通过===
来判断的,如果state
或者prop
的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同;
但是如果前者的类型是复杂的对象的时候,我们知道对象是引用类型,浅层比较只会认为这两个prop
是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop
。
举个例子:
当我们给组件Foo
给名为style
的prop
赋值;
<Foo style={{ color:"red" }}
使用这种方法,每一次渲染都会被认为是一个style
这个prop
发生了变化,因为每一次都会产生一个对象给style
。
那么我们应该如何改进,如果想要让react
渲染的时候认为前后对象类型prop
相同,则必须要保证prop
指向同一个javascript
对象,如下:
const fooStyle = { color: "red" }; //取保这个初始化只执行一次,不要放在render中,可以放在构造函数中
<Foo style={fooStyle} />
这个问题是我们在平时的编码中可以避免的。
2.2,定制shouldComponentUpdate
函数
shouldComponentUpdate
是决定react
组件什么时候能够不重新渲染的函数,但是这个函数默认的实现方式就是简单的返回一个true
。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括render
函数,重新渲染。
我们来看一下下面的一个例子
我们写两个组件,App
和Demo
组件,并写两个方法,一个改变App
中的num
的值,一个是改变title
,我们在Demo的render中打印render函数。我们可以看到以下的效果:
我们可以清晰的看到虽然demo
组件里的title
值没有改变,但是还是render
了。
为了解决这个问题,我们可以对demo组件进行如下的修改:
只有当demo的title值发生改变的时候,我们才去render,我们可以看一下效果:
以上只是一个特别简单的一个对于shouldComponentUpdate
的定制。
在最新的react
中,react给我们提供了React.PureComponent
,官方也在早期提供了名为react-addons-pure-render-mixin
插件来重新实现shouldComponentUpdate
生命周期方法。
通过上述的方法的效果也是和我们定制shouldComponentUpdate
的效果是一致的。
但是我们要注意的是,这里的PureRender
是浅比较的,因为深比较的场景是相当昂贵的。所以我们要注意我们在1.1
中说到的一些注意点:不要直接为props设置对象或者数组、不要将方法直接绑定在元素上,因为其实函数也是对象
2.3,Immutable.js
先配上一张经典的图和经典的一句话:
Shared mutable state is the root of all evil(共享的可变状态是万恶之源)
-- Pete Hunt
javascript
中的对象一般都是可变的,因为使用了引用赋值,新的对象简单的引用了原始对象,改变新对象将影响到原始对象。
举个例子:
foo = { a : 1 };
bar = foo;
bar.a = 2;
当我们给bar.a
赋值后,会发现foo.a
也变成了2,虽然我们可以通过深拷贝与浅拷贝解决这个问题,但是这样做非常的昂贵,对cpu
和内存会造成浪费。
这里就需要用到Immutable
,通过Immutable
创建的Immutable Data
一旦被创建,就不能再更改。对Immutable
对象进行修改、添加或删除操作,都会返回一个新的Immutable
对象。
这里我们将一下其中三个比较重要的数据结构
- Map:键值对集合,对应
Object
,Es6
种也有专门的Map
对象 - List:有序可重复列表,对应于
Array
- ArraySet:有序且不可重复的列表
我们可以看两个例子:
使用Map
生成一个immutable
对象
import { Map , is } from 'immutable';
let obj = Map({
'name': 'react study',
'course': Map({name: 'react+redux'})
})
let obj1 = obj.set('name','darrell');
console.log(obj.get('course') === obj1.get('course')); // 返回true
console.log(obj === obj1); // 返回false
Immutable.is
比较的是两个对象的 hashCode
或 valueOf
(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode
相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
let obj = Map({name:1,title:'react'});
let obj1 = Map({name:1,title:'react'});
console.log(is(obj,obj1)); // 返回true
let obj2 = {name:1,title:'react'};
let obj3 = {name:1,title:'react'};
console.log(is(obj2,obj3)); // 返回false
Immutable
优点:
- 减少内存的使用
- 并发安全
- 降低项目的复杂度
- 便于比较复杂数据,定制shouldComponentUpdate方便
- 时间旅行功能
- 函数式编程
Immutable
缺点:
- 学习成本
- 库的大小(建议使用seamless-immutable)
- 对现有项目入侵严重
- 容易与原生的对象进行混淆
如果大家想深入了解,可以参考immutable、IMMUTABLE 详解。
2.4,多个react组件性能优化,key的优化
react
组件在装载过程中,react
通过在render
方法在内存中产生一个树形结构,树上的节点代表一个react
组件或者原生的Dom
元素,这个树形结构就是我们所谓的Vitural Dom
,react根据这个来渲染产生浏览器的Dom
树。
react
在更新阶段对比原有的Vitural Dom
和新生成的Vitural Dom
,找出不同之处,在根据不同来渲染Dom树。
react为了追求高性能,采用了时间复杂度为O(N)
来比较两个属性结构的区别,因为要确切比较两个树形结构,需要通过O(N^3)
,这会降低性能。
我们举几个情况,大家就会马上理解:
-
节点类型不同
// A组件 <div> <Todos /> </div> // B组件 <span> <Todos /> </span>
我们想把
A
组件更新成B
组件,react
在做比较的时候,发现最外面的根结点不一样,直接就废掉了之前的<div>
节点,包括里面的子节点,这是一个巨大的浪费,但是为了避免O(N^3)
的时间复杂度,只能采用这种方式所以在开发过程中,我们应该尽量避免上面的情况,不要将包裹节点的类型随意改变。
-
两个节点类型一样
这里包括两种情况,一种是节点是
Dom
类型,还有一种react
组件。对于
dom
类型,我们举个例子:// A组件 <div style={{color: 'red',fontSize:15}} className="welcome"> Hello World!!! </div> // B组件 <div style={{color: 'green',fontSize:15}} className="react"> Good Bye!!! </div>
上述A和B组件的区别是文字、
className
、style
中的color
发生改变,因为Dom
元素没变,React
只会修改他变化的部分。针对
react
组件类型,渲染无非就是在走一遍组件实例的更新过程,最主要的就是定制shouldComponentUpdate
,我们上面也有讲到,就不细讲了。 -
多个子组件情况
我们看两个例子就能明白
例子一:
// A <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul> // B <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> <TodoItem text="Third" complete={false} /> </ul>
从A变到B,如果
shouldComponentUpdate
处理得当,我们只需要更新装载third
的那一次就行。我们来看看下一个例子:
// A <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul> // B <ul> <TodoItem text="Zero" complete={false} /> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul>
这里因为react是采用O(n)的时间复杂度,所以会依次将text为First的改为Zero,text为Second改为First,在最后再加上一个组件,text为Second。现存的两个的text的属性都被改变了,所以会依次渲染。
如果我们这里有1000个实例,那么就会发生1000次更新。
这里我们就要用到
Key
了简单来说,其实这一个Key就是react组件的身份证号。
我们将上一个例子改成如下,就可以避免上面的问题了,react就能够知道其实B里面的第二个和第三个组件其实就是A中的第一个和第二个实例。
// A <ul> <TodoItem key={1} text="First" complete={false} /> <TodoItem key={2} text="Second" complete={false} /> </ul> // B <ul> <TodoItem key={0} text="Zero" complete={false} /> <TodoItem key={1} text="First" complete={false} /> <TodoItem key={2} text="Second" complete={false} /> </ul>
不过现在,react也会提醒我们不要忘记使用
key
,如果没有加,在浏览器中会报错。
关于
key
的使用我们要注意的是,这个key值要稳定不变的,就如同身份证号之于我们是稳定不变的一样。一个常见的错误就是,拿数组的的下标值去当做key,这个是很危险的,代码如下,我们一定要避免。
<ul> { todos.map((item, index) => { <TodoItem key={index} text={item.text} completed={item.completed} }) } </ul>
二,redux性能优化:reselect(数据获取时优化)
在前面的优化过程中,我们都是优化渲染来提高性能的,既然react
和redux
都是通过数据驱动的的方式驱动渲染过程,那么处理优化渲染过程,获取数据的过程也是需要考虑的一个优化点。
//下面是redux中简单的一个筛选功能
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps
函数作为redux store
中获取数据的重要一环,当我们根据filter
和todos
来显示相应的待办事项的时候,我们都要遍历todos
字段上的数组。
当数组比较大的时候,则会降低性能。
这个时候,reselect就应运而生了,它的动作原理:只要相关的状态没有改变,那么就直接使用上一次的缓存结果。
具体的用法我就不在这里过多介绍了,已经有很多的牛人写了相关的文章,我也不重复写了,大家如果想深入了解的话,可以参考reselect的giuhub、Redux的中间件-Reselect。
三:参考资料
此篇文章是参考了《深入React技术栈》和《深入浅出React与Redux》这两本书中关于对react性能优化的章节,再加上自己的动手实践与思考写的。
文章中不乏会有些错误的地方还请大家多多批评指正。
希望这篇文章对大家能有帮助,来自一个奔跑在前端路上的前端小白。