Redux + Immutable.js 性能優化

(閱讀本文約需 2 分鐘)javascript

引言

衆所周知,在使用 Redux 時最麻煩的一個部分就是 reducer 的編寫,因爲 Redux 要求狀態是 immutable 的,也就是說,發生變化的狀態樹必定是一個新的引用。 因此 reducer 常常會寫成這樣:java

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}
複製代碼

不少人會稱之爲深克隆,其實並非,這個過程既不是深克隆也不是淺克隆。git

reducer 的正確寫法

首先咱們來談談深克隆是否可行,若是你的 reducer 在每次狀態發生變化時都進行深克隆處理,你的 app 毋庸置疑是能夠 work 的,Time Travelling 固然也能夠用,那麼問題會出在哪裏呢?github

咱們不妨經過圖示來看一下:redux

整個狀態樹被重建了,這就意味着 PureComponentshouldComponentUpdate 沒有實現好的組件都會從新 render。app

因此在實際項目中,咱們引入了 Immutable.js,就是爲了不寫出繁瑣或者不正確的 reducer。相似的還有 immer 這樣的庫。函數

Immutable.js 內部會使用 Shared Structure 來避免深克隆,一方面提高了 Immutable.js 自身的性能,另外一方面能幫助 React 更高效地渲染。就像這樣:性能

當一個對象中的一個鍵發生變化時,這個對象中其餘鍵的值不會有任何變化,而引用該對象的對象會產生一份新的引用,以此類推。這樣,咱們的狀態樹就能夠像值類型同樣進行對比了:優化

節點 4 發生變化,節點 一、2 變化先後必定不相等,可是節點 三、五、6 沒有變化仍然是相等的。咱們甚至不用 deepEquals,對比引用就能夠了,由於 Immutable.js 能夠保證它們不發生變化。ui

所以,咱們的 React 組件若是採用了 PureComponent,就能自動得到最好的優化,與變化無關的組件也不會從新渲染。

Immutable.js 與 React 配合的正確用法

然而在實際使用中,咱們又遇到了問題,即使使用了 Immutable.js,每次更新時仍是有不少無關組件發生更新了。搜查了一遍代碼,我發現咱們如今有不少這樣的寫法:

const mapStateToProps = state => {
  const user = selectCurrentUser(state)
  const me = user.toJS()
  const myTeam = selectMyTeam(state)
  const team = myTeam && myTeam.toJS()
  //...
  return { user, me, myTeam, team /*, ...*/ }
 }
複製代碼

問題就出在 toJS 的調用上,根據文檔:

Deeply converts this Keyed collection to equivalent native JavaScript Object.

toJS 會將本來 structure shared 的對象徹底深克隆一遍,全部 PureComponent 又會從新渲染。能夠看一下咱們如今的狀況:

能夠看到,改變了一個與左側邊欄無關的按鈕狀態的時候,左側邊欄依舊從新渲染了。

下面是去掉了 toJS 調用後的狀況:

是否是好多了。

總結

至此咱們也可以得出結論了,React 的渲染性能很大一部分取決於更新的粒度,當咱們的 render 函數已經足夠龐大時,咱們可以作的只有分步更新(Fiber 和 Time Slicing 主要解決的問題)和精準更新了。

而要作到精準更新,就必定要處理好狀態的變化,其實最簡單的方法就是狀態扁平化,對象層級越小,咱們的代碼裏可能出現的問題就越少。另外,儘量將 connect 放置在須要狀態的組件外,目前咱們仍是有不少組件過早 connect,而後將狀態一層一層經過 props 傳下去,這也是狀態對象層級太深(有多深我就不截圖了...)致使的。Redux 狀態更新(dispatch)時,全部的 Connect(...) 組件都會根據本身的 mapped state 進行更新,越早 connect 的組件越有可能發生更新,而其子組件若是沒有處理好 shouldComponentUpdate 就會出現許多無用的更新,白白損失性能。


References:

  1. Immutable Data Structures and JavaScript
  2. Reducers - Redux
  3. Map -- Immutable.js
相關文章
相關標籤/搜索