React 性能优化,你需要知道的几个点

图片发自简书App

写了一段时间的react之后,渐渐的喜欢上了使用react来写应用。

我们知道,Facebook在推出react时打出的旗号之一就是高性能。

今天我们还一起来聊一聊react的性能优化,思考还能通过哪些手段来提升React的性能,使我们的react更快,性能更好。

一,react组件的性能优化(渲染角度优化)

1,react性能查看工具

再讲性能优化之前,我们需要先来了解一下如何查看react加载组件时所耗费的时间的工具,在react 16版本之前我们可以使用React Perf来查看。

大家可以在chorme中先安装React Perf扩展,然后在入口文件或者reduxstore.js中加入相应的代码即可:

React Perf

在最新的React16版本中,我们可以直接在url后加上?react_pref,就可以在chrome浏览器的performance,我们可以查看User Timeing来查看组件的加载时间。

react16.0_pref

使用此工具的具体操作大家可以看下图:


react16.0_pref.gif

2,单个react组件性能优化

2.1,render里面尽量减少新建变量和bind函数,传递参数是尽量减少传递参数的数量。

首先我们先思考一个问题,比如我要实现一个点击按钮使相应的num增加1,我们有哪一些方法。

大家应该都能想到,无非就是三种,如下图:

react_function

第一种是在构造函数中绑定this,第二种是在render()函数里面绑定this,第三种就是使用箭头函数,都能实现上述方法;

但是哪一种方法的性能最好,是我们要考虑的问题。应该大家都知道答案:第一种的性能最好

因为第一种,构造函数每一次渲染的时候只会执行一遍;

而第二种方法,在每次render()的时候都会重新执行一遍函数;

第三种方法的话,每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。

第三种方法我们可以举一个例子,因为react判断是否需要进行render浅层比较,简单来说就是通过===来判断的,如果state或者prop的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同;

但是如果前者的类型是复杂的对象的时候,我们知道对象是引用类型,浅层比较只会认为这两个prop是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop

举个例子:

当我们给组件Foo给名为styleprop赋值;

<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函数,重新渲染。

我们来看一下下面的一个例子

shouldComponentUpdate

我们写两个组件,AppDemo组件,并写两个方法,一个改变App中的num的值,一个是改变title,我们在Demo的render中打印render函数。我们可以看到以下的效果:

shouldComponentUpdate_demo

我们可以清晰的看到虽然demo组件里的title值没有改变,但是还是render了。

为了解决这个问题,我们可以对demo组件进行如下的修改:

shouldComponentUpdate

只有当demo的title值发生改变的时候,我们才去render,我们可以看一下效果:

shouldComponentUpdate_demo

以上只是一个特别简单的一个对于shouldComponentUpdate的定制。

在最新的react中,react给我们提供了React.PureComponent,官方也在早期提供了名为react-addons-pure-render-mixin插件来重新实现shouldComponentUpdate生命周期方法。

PureComponent

通过上述的方法的效果也是和我们定制shouldComponentUpdate的效果是一致的。

但是我们要注意的是,这里的PureRender是浅比较的,因为深比较的场景是相当昂贵的。所以我们要注意我们在1.1中说到的一些注意点:不要直接为props设置对象或者数组不要将方法直接绑定在元素上,因为其实函数也是对象


2.3,Immutable.js

先配上一张经典的图和经典的一句话:

Immutable

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:键值对集合,对应ObjectEs6种也有专门的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 比较的是两个对象的 hashCodevalueOf(对于 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)
  • 对现有项目入侵严重
  • 容易与原生的对象进行混淆

如果大家想深入了解,可以参考immutableIMMUTABLE 详解


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组件的区别是文字、classNamestyle中的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,如果没有加,在浏览器中会报错。

    react_key

    关于key的使用我们要注意的是,这个key值要稳定不变的,就如同身份证号之于我们是稳定不变的一样。

    一个常见的错误就是,拿数组的的下标值去当做key,这个是很危险的,代码如下,我们一定要避免。

    <ul>
      {
            todos.map((item, index) => {
                <TodoItem
                  key={index}
                  text={item.text}
                  completed={item.completed}
            })
      }
    </ul>
    


二,redux性能优化:reselect(数据获取时优化)

在前面的优化过程中,我们都是优化渲染来提高性能的,既然reactredux都是通过数据驱动的的方式驱动渲染过程,那么处理优化渲染过程,获取数据的过程也是需要考虑的一个优化点。

//下面是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中获取数据的重要一环,当我们根据filtertodos来显示相应的待办事项的时候,我们都要遍历todos字段上的数组。

当数组比较大的时候,则会降低性能。

这个时候,reselect就应运而生了,它的动作原理:只要相关的状态没有改变,那么就直接使用上一次的缓存结果。

具体的用法我就不在这里过多介绍了,已经有很多的牛人写了相关的文章,我也不重复写了,大家如果想深入了解的话,可以参考reselect的giuhubRedux的中间件-Reselect


三:参考资料

此篇文章是参考了《深入React技术栈》和《深入浅出React与Redux》这两本书中关于对react性能优化的章节,再加上自己的动手实践与思考写的。

文章中不乏会有些错误的地方还请大家多多批评指正。

希望这篇文章对大家能有帮助,来自一个奔跑在前端路上的前端小白。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 说在前面 关于 react 的总结过去半年就一直碎碎念着要搞起来,各(wo)种(tai)原(lan)因(le)。心...
    陈嘻嘻啊阅读 6,843评论 7 41
  • 初学者对React可能满怀期待,觉得React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都...
    梭子蟹_3984阅读 533评论 0 1
  • 3. JSX JSX是对JavaScript语言的一个扩展语法, 用于生产React“元素”,建议在描述UI的时候...
    pixels阅读 2,804评论 0 24
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,809评论 1 18
  • 每年都会有一个冬天,却不是每年都有一个未曾释怀的梦。别人说黑夜漫长,但冬天的夜如你一般,如此短暂。 天空晕上大雪的...
    西柚子茶LM阅读 255评论 0 6