Redux Optimization

前言

经常有人会觉得,使用 react/redux 的 web 应用性能会很差。大多数的情况下,这种性能的问题都来源于不必要的重复渲染,因为 DOM 的更新代价是十分昂贵的。

实际上在开发时,尤其是刚接触 React/redux 应用开发时,非常容易犯一些错误而引起这种重复的渲染。

quick dive into react-redux

我们通常会使用 connect 这个高阶组件来使 React 组件订阅 redux store 中的变化,来更新这一组件和它的子组件。在 react-redux 中,已经对 connect 做了一定的优化,它的完整签名应该是这样子的:

return function connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
    pure = true,
    areStatesEqual = strictEqual,
    areOwnPropsEqual = shallowEqual,
    areStatePropsEqual = shallowEqual,
    areMergedPropsEqual = shallowEqual,
    ...extraOptions
  } = {}
) {
  ...
}

在我们的实际使用中,大多数情况下我们都只会传入前两个参数,但通过这个签名我们可以更好的理解它内部的逻辑。

所有通过 connect 传入的参数,都是用来生成一个对象,然后传入被包裹的组件来作为 props。

mapStateToProps 是用来从 redux store 中抽取需要的状态数据来生成一个新的对象; 而 mapDispatchToProps 用来生成一个带有函数的对象,通常这些函数都是用来生成 action 的; 默认情况下 mergeProps 则把前两个参数生成的 stateProps, dispatchProps 和组件自己的 ownProps 来组合成一个 object,如果你传入了一个函数作为这个参数,那么则会按照你传入的这个函数来组合; 最后这一个参数,类似 shouldComponentUpdate 一样,可以按照你指定的选项来确定是否应该 re-render 组件,默认的情况会当作 pureComponent 一样处理。

那什么叫 pure 呢?其实翻一下 react 的文档就可以看到,pureComponent 同普通 react component 的区别就在于默认就引入了 shouldComponentUpdate 来进行 shallowEqual 比较 props 和 state。

所谓的 shallow,就是只是循环比较 object 的每个属性,执行samevaluezero判断,但对于属性内更深的层次,则不会去比较了。例如我们可以看看 lodash 中的 _.eq_.isEqual

var object = {
  a: {
    b: 1
  }
}

var other = {
  a: {
    b: 1
  }
}

_.eq(object, other) // false, 虽然这两个对象看上去相同,但他们的 a 属性是不同的引用,指向了不同的 { b: 1 }
_.isEqual(object. other)  // true

因此我们可以得出一个非常重要的结论,那就是只传递给你的组件它需要的数据,否则多传入的冗余的数据的变化,也会引起组件的 re-render,这就造成了性能的浪费。

依据这一个结论,我们就可以推导出一些基本的原则。

分割 connected 的组件

我们有时候会看到这种情况,使用一个大的 container 组件来获取所有的状态,然后通过 props 分发给内部的子组件:

const BigComponent = ({ a, b, c, d }) => (
  <div>
    <ComponentA a={a}/>
    <ComponentB b={b}/>
    <ComponentC c={c}/>
  </div>
)

const connectedBigComponent = connect(
  ({ a, b, c }) => ({ a, b, c })
)(BigComponent)

现在,只要是 a,b,c 状态中的任何一个改变,那么整个 BigComponent,包括其中的三个子组件,都会 re-render。然而实际上,我们只需要 a 状态改变时,CompopnentA 重新渲染就可以了,b,c 的改变,对它不应该有任何影响。

将状态转化为尽可能的小和简单

举个例子,我们有一个很大的列表,比如好几百个:

const List = (props) => (
  <ul>
    {
      props.items.map(({ content, itemId }) => (
        <ListItem
          onClick={onSelectItem}
          content={content}
          itemId={itemId}
          key={itemId}
        />
      ))
    }
  </ul>
)

当我们点击其中一个的时候,会发起一个 action 到 store 来更新当前选中是哪一个 - selectedItem,每一个 ListItem 会 connect 到 store 来获取这个 selectedItem

const ListItem = connect(
  ({ selectedItem }) => ({ selectedItem })
)(SimpleListItem)

注意我们之前说的上一条原则,这里如果我们 connect 的是 List 这个组件,那么 selectedItem 改变的时候整个组件以及几百个子组件都会更新,这显然不是我们想要的。所以我们需要 connect 单独的每一个子组件。

但是如果像上面的代码一样直接把 selectedItem 的值传入,所有的子组件可能还是会更新一遍,因为 props 可能从 { selectedItem: 100 } 变成了 { selectedItem: 200 },而我们实际上只需要根据 id 来检查当前的是否被选中,因此我们最好转化为:

const ListItem = connect(
  ({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })
)(SimpleListItem);

这样,每当 selectedItem 的值改变时,只有两个 ListItem 会被重新渲染。

扁平化的数据结构

这一点其实在 Redux 文档中有比较详细的说明。

ref