Redux + Immutable.js 效能優化

Cyandev發表於2018-05-20

(閱讀本文約需 2 分鐘)

引言

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

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
  }
}
複製程式碼

很多人會稱之為深克隆,其實並不是,這個過程既不是深克隆也不是淺克隆。

reducer 的正確寫法

首先我們來談談深克隆是否可行,如果你的 reducer 在每次狀態發生變化時都進行深克隆處理,你的 app 毋庸置疑是可以 work 的,Time Travelling 當然也可以用,那麼問題會出在哪裡呢?

我們不妨通過圖示來看一下:

Redux + Immutable.js 效能優化

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

所以在實際專案中,我們引入了 Immutable.js,就是為了避免寫出繁瑣或者不正確的 reducer。類似的還有 immer 這樣的庫。

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

Redux + Immutable.js 效能優化

當一個物件中的一個鍵發生變化時,這個物件中其他鍵的值不會有任何變化,而引用該物件的物件會產生一份新的引用,以此類推。這樣,我們的狀態樹就可以像值型別一樣進行對比了:

節點 4 發生變化,節點 1、2 變化前後一定不相等,但是節點 3、5、6 沒有變化仍然是相等的。我們甚至不用 deepEquals,對比引用就可以了,因為 Immutable.js 可以保證它們不發生變化。

因此,我們的 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 又會重新渲染。可以看一下我們現在的情況:

Redux + Immutable.js 效能優化

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

下面是去掉了 toJS 呼叫後的情況:

Redux + Immutable.js 效能優化

是不是好多了。

總結

至此我們也能夠得出結論了,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

相關文章