理解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
- 具体代码实现
性能监控
对于性能上出现的问题,及时发现及时优化是很重要的,但在前端监控上,网上都是千篇一律的收集浏览器的performance api的指标,这种指标只能对于页面首次加载,无法收集页面运行时性能。对此我想出了一种方案:
什么是运行时性能
比如vue组件,当触发了click事件之后,记录一个开始时间,然后在updated生命周期记录结束时间,然后计算时间差,这个就是组件更新的一个性能了。同理react组件,就是在useLayoutEffect记录计算时间差
解决方案
每个组件都要去做以上的逻辑,太麻烦了。其实我们就是想要一个开始时间和渲染后的结束时间
- 开始时间可以把click事件绑定在document上,只要页面上发生了点击就记录一个开始时间,为了避免事件被阻止冒泡、时间在冒泡过程中在处理处理业务逻辑,需要把事件改成捕获模式
- 每次事件触发后都去生成一个requestIdleCallback,然后在其回调函数中计算时间差
- 采用requestIdleCallback的原因是,他的回调函数是在浏览器空闲的时候执行的,他的优先级低于任何其他异步任务,而react、vue的组件更新是放在异步队列中的,会在requestIdleCallback之前完成视图更新
- 但是会有个问题,因为是监听了所有的点击操作,即使是无效的空白地方也会触发事件。经过我的测试,无效点击因为没有产生任何逻辑,其间隔时间不会超过5ms,在数据上报的时候可以过滤掉这些数据
最后
知识无止境,更深更广的知识面能够更好地应付以后的突发需求,但有时候知识不一定要落地,额外的性能优化手段会增加代码复杂度需要一定的维护成本,有时候用React自带的性能优化api已经足够;要评估好业务的规模,不要为了一个永远都不会遇到的瓶颈而去浪费时间