標籤:

redux 中的 state 樹太大會不會有性能問題?


之前考慮過這個問題,參考了一些討論,這裡大概總結一下。

所謂的『性能問題』主要可能存在以下幾個方面:

- action觸發reducer的開銷。

用redux-ignore(GitHub - omnidan/redux-ignore: higher-order reducer to ignore redux actions)完全防止觸發跟某reducer不相關的action。

感謝@a mao li指正低級錯誤,combineReducer只是語法糖,action還是會走一遍所有reducer的。

- tree的存儲開銷。(不是問題)

所謂的state tree就是一個object,裡面的reference了其他object。JS里的object是傳址pass/store,也就是一個整數形態的pointer而已。

- 通過connect()連接store的subscriber,在store改變時被觸發的開銷。

如果每個UI component都連接一堆props到store自然是很恐怖的,因此 1) 在high-level component做connect,然後由其負責的component渲染(不連接store);2) 只屬於該container的UI操作(比如開關按鈕)等等的放到state而不是store里。

- 渲染一棵大樹到DOM的開銷。

首先React只渲染改變的subtree,這是React / Immutable || Redux (obj.spread)美妙之處,寫過Angular的應該會有感悟。然後還可以通過shouldComponentUpdate()來強制不渲染,這也是第一個可以輕鬆完成的性能優化。

Dan(redux作者)做過一個壓力測試,結論是渲染一萬個node(好像用的是counter的例子)時,操作會有很輕微的延遲。(某些framework一把冷汗)

正常的app應該是不會有上千node的,假如真的有,那絕大部分都不是用戶可見的(比如一個很長很長的scroll view)。這樣情況下,可以用react-infinite(GitHub - seatgeek/react-infinite: A browser-ready efficient scrolling container based on UITableView),原理是只渲染用戶能看見,以及即將看見的。其餘的都合併起來放在一個dummy node里。

最後,最重要的優化是:沒問題之前不做優化。


先測量,再談優化。其次,可以考慮用immutable的數據結構來優化。


對於redux中state樹(state tree)的性能,可以分為以下幾個方面討論:

------------------------------------------------------------------------------------------------

1.讀取(read)

抽象一下createStore中與read相關部分的代碼如下,

function createStore(reducer, initialState, enhancer){
var currentState = initialState

function getState(){
return currentState
}

return {
getState
}
}

所以對於read來說,state tree變得越大,並不會影響store.getState()的性能。read的性能主要取決於store中存儲的state本身的read效率。

------------------------------------------------------------------------------------------------

2.修改(modify)

對於一個很「大」的state tree來說,它的「大」體現在:

* state tree中存儲了體積較「大」的數據

例如:store tree中存儲了一個大小為500kb的對象(object)A。當我們不斷地對A進行修改時,由於redux中reducer的要求(immutable,見下圖**部分),對象A會被不斷地拷貝(copy),形成新的redux tree並返回。

Things you should never do inside a reducer:
**Mutate its arguments**
Perform side effects like API calls and routing transitions
Calling non-pure functions, e.g. Date.now() or Math.random()

對於這種情況,redux的作者指出了一個常被誤解的觀念:

比如{a:1,b:{c:2,d:4}}其實不是一個體積很大的object,它僅僅是一個含有兩個指向了其他object的屬性(field)的object。因為在javascript中所有的object都是通過引用(reference)來傳遞與存儲了,而reference的內部實現是用數字(pointer)來存儲。

所以我們對state tree不斷地更新與修改,我們只需要對其中需要更新的那一小部分進行重新分配內存(re-creating)就可以了,此時state tree中其他部分的reference依然指向原有的object。

則我們re-creating一個state tree的時間 = T(re-creating一個由reference組成的小object) + T(創造一個用來替換被修改部分的新object)。

對於 T(創造一個用來替換被修改部分的新object) 來說,可以使用Immutable.js來提高性能,它可以有效的減少不必要的copy與cache data,題主可以嘗試一下使用它來保證性能。

以上,redux中的state tree對於存儲體積較大的object來說並不存在的性能問題。

* state tree的邏輯「結構複雜」,使用了複雜的reducer

(下次再寫,霧)

------------------------------------------------------------------------------------------------

以上


分析造成State樹龐大的原因:

  1. 本不改變state的這些action被dispatch出去,造成額外的state copy,這種情形可以採用GitHub - omnidan/redux-ignore: higher-order reducer to ignore redux actions

  2. state存在多級複雜對象嵌套,現在有兩種解決方式:
    1. 對某個reducer,手動合併部分需要的state,由於ES7的Object Spread Operator(...state)只會拷貝enumerable屬性,並且只會拷貝一層,因此需要手動合併屬性,比如

      state: {
      a: "foo",
      b: {
      c: "bar"
      }
      }

      return {
      ...state,
      b: {
      ...state.b,
      d: "baz"
      }
      }

    2. 如果state的某些屬性過於龐大,而不是複雜,可以採用Immutable.js,通過持久化數據結構和重用部分樹節點,實現高效返回一個新的不可變對象,在保證了reducer的純函數性前提下,不僅免去手寫DeepMerge的繁瑣,又能達到高效的"修改"(即返回一個部分屬性改變的新的不可變對象)部分屬性的性能。在你的component的shoudComponentUpdate方法中,通過Immutable的比較方法來判斷是否需要re-render(常見的ListView則可以用dataSource的rowHasChanged方法比較Immutable對象)

      let nested = Immutable.fromJS({a:{b:{c:[3,4,5]}}});
      let nested2 = nested.mergeDeep({a:{b:{d:6}}});
      // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 6 } } }
      let nested3 = nested2.updateIn(["a", "b", "d"], value =&> value + 1);
      // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 7 } } }

    3. 如果state本身基本只和數據相關(和控制流交互狀態關係較少,可以通過拆分大的reducer來達到),可以直接把state本身用Immutable.Map之類包裹起來,在store綁定到component的時候解包
  3. 其他情況更多是架構問題,對細度小的同一類事件的state不應該單獨分,可以歸類成一個對象的屬性來處理,對性能幾乎無影響,但統一處理之後能夠讓整個reducer更清晰。同時,state的管理,有三處位置(reducer,connect,shouldComponentUpdate),需要靈活使用,保證可維護性和效率的均衡


性能問題一般情況下不會有太大影響,但是redux會將所有組件的state全部都扔進一個大的state中去維護,每次reducer只會更新一小部分。這要求開發者要自行構建良好的state組織結構,否則當頁面組件越來越多時,很容易被一個超級大的state繞暈。

我們現在在項目中使用了redux-arena,它可以將redux與react打包成一個模塊載入,如果react組件被卸載,那麼react組件在redux中的state/reducer/saga都會被自動卸載,徹底解決state樹和reducer過於龐大的問題。

一張gif圖說明redux-arena做了什麼,如下圖所示,當切換pageA和asyncPageB的掛載和卸載,會觸發redux中全局state樹的改變,所有與當前頁面展示無關的state和reducer徹底不復存在,你看到的state一定是當前被掛載的組件的state,極大地提升了state的可讀性。

項目的github地址:hapood/redux-arena

還有一篇相關介紹在這裡:https://zhuanlan.zhihu.com/p/28690716


homkai/react-redux-hk

當connect的組件太多時,用這個替換react-redux,可以提高性能,不需要業務上特殊處理


但是我在做一個React Native項目的過程中,碰到的一個現象是,有一個非同步請求,數據量大概是j幾百條數據,通過redux存儲之後,頁面操作就變得相當卡,特別是頁面跳轉(跳轉之前要dispatch action), 只要不走發這個大數據請求,整個APP是相當流暢的?個人認為性能瓶頸是在redux, 各位大神有沒有什麼性能檢測方法,求推薦....


會有,所以建議用reselect


推薦閱讀:

redux中所有state都要放在store里嗎?

TAG:React | Redux |