Redux + Immutable.js 性能优化

4,620

(阅读本文约需 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 当然也可以用,那么问题会出在哪里呢?

我们不妨通过图示来看一下:

整个状态树被重建了,这就意味着 PureComponentshouldComponentUpdate 没有实现好的组件都会重新 render。

所以在实际项目中,我们引入了 Immutable.js,就是为了避免写出繁琐或者不正确的 reducer。类似的还有 immer 这样的库。

Immutable.js 内部会使用 Shared Structure 来避免深克隆,一方面提升了 Immutable.js 自身的性能,另一方面能帮助 React 更高效地渲染。就像这样:

当一个对象中的一个键发生变化时,这个对象中其他键的值不会有任何变化,而引用该对象的对象会产生一份新的引用,以此类推。这样,我们的状态树就可以像值类型一样进行对比了:

节点 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 又会重新渲染。可以看一下我们现在的情况:

可以看到,改变了一个与左侧边栏无关的按钮状态的时候,左侧边栏依旧重新渲染了。

下面是去掉了 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