React 的性能优化(一)当 PureComponent 赶上 ImmutableJS

1、痛点

在咱们的印象中,React 好像就意味着组件化、高性能,咱们永远只须要关心数据总体,两次数据之间的 UI 如何变化,则彻底交给 React Virtual DomDiff 算法 去作。以致于咱们很随意的去操纵数据,基本优化shouldComponentUpdate 也懒得去写,毕竟不写也能正确渲染。但随着应用体积愈来愈大,会发现页面好像有点变慢了,特别是组件嵌套比较多,数据结构比较复杂的状况下,随便改变一个表单项,或者对列表作一个筛选都要耗时 100ms 以上,这个时候咱们就须要优化了!固然若是没有遇到性能瓶颈,彻底不用担忧,过早优化是邪恶的。这里咱们总结一个很简单的方案来让 React 应用性能发挥到极致。在下面一部分,咱们先回顾一下一些背景知识,包括:JavaScript 变量类型和 React 渲染机制,若是你是老鸟能够直接跳过。javascript

2、一些背景知识的回顾

1. 变量类型

JavaScript的变量类型有两类:html

  • 基本类型:6 种基本数据类型, UndefinedNullBooleanNumberStringSymbol
  • 引用类型:统称为 Object 类型,细分为:Object 类型、 Array 类型、 Date 类型、 RegExp 类型、 Function 类型等。

举个例子:java

let p1 = { name: 'neo' };
let p2 = p1;
p2.name = 'dave';
console.log(p1.name); // dave复制代码

在引用类型里,声明一个 p1 的对象,把 p1 赋值给 p2 ,此时赋的实际上是该对象的在堆中的地址,而不是堆中的数据,也就是两个变量指向的是同一个存储空间,后面 p2.name 改变后,也就影响到了 p1。虽然这样作能够节约内存,但当应用复杂后,就须要很当心的操做数据了,由于一不注意修改一个变量的值可能就影响到了另一个变量。若是咱们想要让他们不互相影响,就须要拷贝出一份如出一辙的数据,拷贝又分浅拷贝与深拷贝,浅拷贝只会拷贝第一层的数据,深拷贝则会递归全部层级都拷贝一份,比较消耗性能。react

2. React

React 中,每次 setStateVirtual DOM 会计算出先后两次虚拟 DOM 对象的区别,再去修改真实须要修改的 DOM 。因为 js 计算速度很快,而操做真实 DOM 相对比较慢,Virtual DOM 避免了不必的真实 DOM 操做,因此 React 性能很好。但随着应用复杂度的提高, DOM 树愈来愈复杂,大量的对比操做也会影响性能。好比一个 Table 组件,修改其中一行 Tr 组件的某一个字段, setState 后,其余全部行 Tr 组件也都会执行一次 render 函数,这实际上是没必要要的。咱们能够经过 shouldComponentUpdate 函数决定是否更新组件。大部分时候咱们是能够知道哪些组件是不会变的,根本就不必去计算那一部分虚拟 DOMgit

3、 PureComponent

React15.3 中新加了一个类PureComponent,前身是 PureRenderMixin ,和 Component 基本同样,只不过会在 render 以前帮组件自动执行一次shallowEqual(浅比较),来决定是否更新组件,浅比较相似于浅复制,只会比较第一层。使用 PureComponent 至关于省去了写 shouldComponentUpdate 函数,当组件更新时,若是组件的 propsstategithub

  1. 引用和第一层数据都没发生改变, render 方法就不会触发,这是咱们须要达到的效果。
  2. 虽然第一层数据没变,但引用变了,就会形成虚拟 DOM 计算的浪费。
  3. 第一层数据改变,但引用没变,会形成不渲染,因此须要很当心的操做数据。

4、 Immutable.js

Immutable.jsFacebook2014 年出的持久性数据结构的库,持久性指的是数据一旦建立,就不能再被更改,任何修改或添加删除操做都会返回一个新的 Immutable 对象。可让咱们更容易的去处理缓存、回退、数据变化检测等问题,简化开发。而且提供了大量的相似原生 JS 的方法,还有 Lazy Operation 的特性,彻底的函数式编程。web

import { Map } from "immutable";
const map1 = Map({ a: { aa: 1 }, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1 !== map2; // true
map1.get('b'); // 2
map2.get('b'); // 50
map1.get('a') === map2.get('a'); // true复制代码

能够看到,修改 map1 的属性返回 map2,他们并非指向同一存储空间,map1 声明了只有,全部的操做都不会改变它。算法

ImmutableJS 提供了大量的方法去更新、删除、添加数据,极大的方便了咱们操纵数据。除此以外,还提供了原生类型与 ImmutableJS 类型判断与转换方法:chrome

import { fromJS, isImmutable } from "immutable";
const obj = fromJS({
  a: 'test',
  b: [1, 2, 4]
}); // 支持混合类型
isImmutable(obj); // true
obj.size(); // 2
const obj1 = obj.toJS(); // 转换成原生 `js` 类型复制代码

ImmutableJS 最大的两个特性就是: immutable data structures(持久性数据结构)与 structural sharing(结构共享),持久性数据结构保证数据一旦建立就不能修改,使用旧数据建立新数据时,旧数据也不会改变,不会像原生 js 那样新数据的操做会影响旧数据。而结构共享是指没有改变的数据共用一个引用,这样既减小了深拷贝的性能消耗,也减小了内存。好比下图:
express

tree
tree

左边是旧值,右边是新值,我须要改变左边红色节点的值,生成的新值改变了红色节点到根节点路径之间的全部节点,也就是全部青色节点的值,旧值没有任何改变,其余使用它的地方并不会受影响,而超过一大半的蓝色节点仍是和旧值共享的。在 ImmutableJS 内部,构造了一种特殊的数据结构,把原生的值结合一系列的私有属性,建立成 ImmutableJS 类型,每次改变值,先会经过私有属性的辅助检测,而后改变对应的须要改变的私有属性和真实值,最后生成一个新的值,中间会有不少的优化,因此性能会很高。

5、 案例

首先咱们看看只使用 React 的状况下,应用性能为何会被浪费,代码地址:github.com/wulv/fe-exa… ,这个案例使用 create-react-app,检测工具使用 chrome 插件:React Perf。执行

git clone https://github.com/wulv/fe-example.git
cd fe-example/react-table
yarn
yarn start复制代码

能够打开页面,开始记录,而后随便对一列数据进行修改,结束记录,能够看到咱们仅修改了一行数据,但在 Print Wasted 那一项里,渲染 Tr 组件浪费了5次:

react-table
react-table

不管是添加,删除操做,都会浪费 n-1render ,由于 App 组件的整个 state 改变了,全部的组件都会从新渲染一次,最后对比出须要真实 DOM 的操做。咱们把 Table 组件和 Tr 继承的 Component 改为 PureComponent ,那么, Tr 组件每次更新都会进行一次 shallowEqual 比较,在记录一次,会发现修改操做没有了浪费,然而这个时候添加和删除操做却无效了,分析一下添加的操做是:

add = () => {
    const  { data } = this.state;
    data.push(dataGenerate())
    this.setState({
      data
    })
  }复制代码

data.push 并无改变 data 的引用,因此 PureComponentshallowEqual 直接返回了 true ,不去 render 了。这并非咱们想要的,因此若是使用 Component 一定带来性能浪费,使用 PureComponent 又必须保证组件须要更新时,propsstate 返回一个新引用,不然不会更新 UI

这个时候, ImmutableJS 就能够显示出它的威力了,由于它能够保证每次修改返回一个新的 Object,咱们看看修改后的例子:代码地址:github.com/wulv/fe-exa… ,执行上面例子一样的操做,能够看到:

react-immutablejs
react-immutablejs

添加,删除,修改操做,没有一次浪费。没有浪费的缘由是全部的子组件都使用了 PureComponentImmutableJS 保证修改操做返回一个新引用,而且只修改须要修改的节点( PureComponent 能够渲染出新的改动),其余的节点引用保持不变( PureComponent 直接不渲染)。能够看出, PureComponentImmutableJS 简直是天生一对啊,若是结合 redux ,那就更加完美了。由于 reduxreducer 必须每次返回一个新的引用,有时候咱们必须使用 clone 或者 assign 等操做来确保返回新引用,使用 ImmutanleJS 自然保证了这一点,根本就不须要 lodash 等函数库了,好比我使用 redux + immutable + react-router + express 写了一个稍微复杂点的例子: github.com/wulv/fe-exa… pageIndexstore 的状态是:

{
  loading: false,
  tableData: [{
    "name": "gyu3w0oa5zggkanciclhm2t9",
    "age": 64,
    "height": 121,
    "width": 71,
    "hobby": {
      "movie": {
        "name": "zrah6zrvm9e512qt4typhkt9",
        "director": "t1c69z1vd4em1lh747dp9zfr"
      }
    }
  }],
  totle: 0
}复制代码

若是我须要快速修改 width 的值为90,比较一下使用深拷贝、 Object.assignImmutableJS 三种方式的区别:

// payload = { name: 'gyu3w0oa5zggkanciclhm2t9', width: 90 }
// 1. 使用深拷贝
 updateWidth(state, payload) {
    const newState = deepClone(state);
    return newState.tableData.map(item => {
      if (tem.name === payload.name) {
        item.width = payload.width;
      }
      return item;
    });
  }
// 2. 使用Object.assign
 updateWidth(state, payload) {
    return Object.assign({}, state, {
      tableData: state.state.map(item => {
        if (item.name === payload.name) {
          return Object.assign({}, item, { width: payload.width });
        }
        return item;
      })
    })
  }
// 3. 使用ImmutableJS
 updateWidth(state, payload) {
  return state.update('tableData', list => list.update(
      list.findIndex((item) => item.get('name') === payload.name),
    item => item.set('width', payload.width)));
  }复制代码

使用深拷贝是一个昂贵的操做,并且引用都改变了,必然形成 re-render, 而 Object.assign 会浅复制第一层,虽然不会形成 re-render,但浅复制把其余的属性也都复制了一次,在这里也是很没有必要的,只有使用 ImmutableJS 完美的完成了修改,而且代码也最少。

6、 优点与不足

能够看出, ImmutableJS 结合 PureComponent 能够很大程度的减小应用 re-render 的次数,能够大量的提升性能。但仍是有一些不足的地方:

  1. 获取组件属性必须用 getgetIn 操做(除了 Record 类型),这样和原生的.操做比起来就麻烦多了,若是组件以前已经写好了,还须要大量的修改。
  2. ImmutableJS 库体积比较大,大概56k,开启 gzip 压缩后16k。
  3. 学习成本。
  4. 难以调试,在 redux-logger 里面须要在 stateTransformer 配置里执行 state.toJS()

7、 最佳实践

其实,重要的是编程者须要有性能优化的意识,熟悉 js 引用类型的特性,了解事情的本质比会使用某个框架或库更加剧要。用其余的方法也是彻底能够达到 ImmutableJS 的效果,好比添加数据可使用解构操做符的方式:

add = () => {
    const  { data } = this.state;
    this.setState({
      data: [...data, dataGenerate()]
    })
  }复制代码

只不过若是数据嵌套比较深,写起来仍是比较麻烦。如下有一些小技巧:

  1. 还有两个轻量库能够实现不可变数据结构:seamless-immutable或者immutability-helper,只不过原理彻底不同,效率也没那么高。
  2. 避免大量使用 toJS 操做,这样会浪费性能。
  3. 不要将简单的 JavaScript 对象与 Immutable.JS 混合
  4. 结合 redux 的时候,要使用import { combineReducers } from 'redux-immutablejs';,由于 reduxcombineReducers 指望 state 是一个纯净的 js 对象。
  5. 尽可能将 state 设计成扁平状的。
  6. 展现组件不要使用 Immutable 数据结构。
  7. 不要在 render 函数里一个 PureComponent 组件的 props 使用 bind(this) 或者 style={ { width: '100px' } },由于 shallowEqual 必定会对比不经过。

8、 参考连接

本文首发于有赞技术博客

相关文章
相关标签/搜索