序言
时光荏苒,在一家公司从毕业到现在工作了五年;为了寻求突破和自身发展,不得已忍痛提出离职。 感谢领导们的信任和同事们的配合,能够让我在团队中淋漓尽致地发挥所学所长。 在做离职交接的过程中也给大家整理了一下之前工作遇到的React性能问题以及优化方案,希望对大家以后的工作有帮助。
理解React设计
关于React性能优化,脑子里自然会浮现出shouldComponentUpdate React.memo; 但一股脑的使用React提供的优化api,有时候会发现性能也没有任何提升;要真正做到性能优化,得先知道React的更新流程:
- 当触发一次setState之后,React会从根节点开始遍历整颗树,对每个节点进行处理。
- React有自带性能优化策略,当一个节点的props、context没有变化且没有调用setState,那么该节点会进行复用。
- 当前节点还提供了一个childLanes字段,用于判断子孙节点是否存在更新,如果子孙节点没有更新,则会复用整颗子树,遍历会提前结束。
- childLanes解释:当一个子组件调用了setState,或者子组件里面使用了context且context的值变化了,那么其祖先节点就会附带上childLanes,至于具体怎么附带上去这里不展开讲。
- 因此,我们做性能优化的目标,就是要减少每个组件的props、context的变化;让React尽可能地对树进行复用
实践
如何优化props
当一个组件发生了setState更新后:
- 会重新生成新的虚拟dom,虚拟dom里包含了props等属性
- 传给子组件的props引用也就发生了变化,会导致子组件更新、重新生成虚拟dom
- 进而影响子组件的子组件props也发生了变化,一直传染下去,导致整颗树都处理了一遍
这种情况就需要使用React.memo或者shouldComponentUpdate,其原理就是对props里面的每一个属性进行浅比较、而不是直接判断props引用
如何优化Context
当你你把所有组件都加上了memo或shouldComponentUpdate,发现一点用都没有,此时就要思考一下context的设计。
- 例子:
- 在根组件定义了一个context,里面包含了a、b、c三个字段数据
- 有三个子组件,A组件使用了a、c两个数据,B组件使用了a、b数据,C组件使用了b、c数据
- C是个耗性能的组件,当Context里的a数据变了,C组件还是会重现渲染,即使加了React.memo和shouldComponentUpdate
- 由于React有着数据不可变性的原则,修改Context的某一个数据,只能生成一个新的Context,而不能在原来的Context上修改某个字段的数据,所以无论哪一个字段的更新都会导致整个context发生了变化
- 解决方案就是把性能消耗大的组件b、c数据单独抽离出来做成一个单独的Context
- 拆分context对组件具有一定侵入性,需要到每个组件中修改context来源
我们的业务使用context的组件没有那么多,所以直接对context进行了拆分;但是对于dva,因为基本所有组件都会用到dva,所以不能直接拆用侵入性的方案,如下:
如何优化Dva、Redux
- 场景

- 当我们在每个组件当中通过useSelector引用了global
- 可以通过globalData访问a、b数据,也可以访问其他在global下的其他字段,在开发过程中很方便
- 但是随之而来的就是跟context类似的问题,例如globalData有个c数据,A组件用到了a、b字段,B组件用到了c字段,当c字段被修改时,A组件也会更新
- 解决方案一:
和context一样进行数据拆分,按需引入;但是这种方案有侵入性,且很不灵活,如果后续要用到其他字段,需要一直新增useSelector
- 解决方案二:
这里先说明一下redux、dva的数据变化之后会发生更新的原理:
- useSelector内部调用了redux提供的subscribe方法订阅了数据变化
- useSelector内部监听到数据变化后,做了setState操作
针对以上逻辑,我们可以自己实现useSelector:
- 获取当前组件使用到了哪些字段,可以通过proxy拦截数据的方案来实现,
- 收到订阅通知后,不直接做setState操作,而是判断数据里面是否包含了对应的字段,如果包含才调用setState
- 具体代码实现

最后
知识无止境,更深更广的知识面能够更好地应付以后的突发需求,但有时候知识不一定要落地,额外的性能优化手段会增加代码复杂度需要一定的维护成本,有时候用React自带的性能优化api已经足够;要评估好业务的规模,不要为了一个永远都不会遇到的瓶颈而去浪费时间