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、 參考連接

本文首發於有贊技術博客

相關文章
相關標籤/搜索