大家好,我卡颂。
虽然主流前端框架都遵循:
- 状态驱动视图
- 单向数据流
理论上并不存在某一框架可以实现,其他框架无法实现的特性。
但是,确实存在某些框架(比如Vue、Qwik)可以,但React无法解决的问题。这就是「极致性能优化」问题。
本文来聊聊React性能优化无法解决的问题。
props下钻
前端框架普遍遵循「单向数据流」。既然是单向数据流,那就存在跨组件传递props的情况。
这种情况被称为「props下钻」(props drilling)。
比如,在下面的应用中:
- <App/>组件定义状态number。
- <AGrandChild/>组件消费number。
- <BGrandChild/>组件包含改变number的方法setNumber。
这种将props(这里的number)层层向下传递(从<App/>到<AGrandChild/>)的情况,就是「props下钻」:
「props下钻」是非常常见的场景。面对这种场景,React的性能怎么样呢?
props下钻的性能
思考一个问题:对于上面的例子,当调用<BGrandChild/>中的setNumber方法改变number后,哪些组件会重新render?
答案是:<App/>的所有子孙组件都会重新render。
这显然与我们的预期不符。
直觉上看,起码<B/>、<C/>及其子孙组件不应该render,毕竟他们都不依赖number。
为了达到这个目标,我们需要使用React.memo包裹<B/>、<C/>,这显然会带来额外的心智负担。
为了减少开发者的心智负担,在2021年的React Conf,黄玄带来了React Forget编译器,他能够为现有业务代码生成等效于useMemo、useCallback的代码。
也就是说,理想情况下,他能够代替开发者完成React项目的性能优化。
但是,回到我们的例子会发现 —— 即使做了性能优化,也无法达到最理想的状态。
整个应用中只有<AGrandChild/>消费了number,理想情况下,当number变化后,应该只有<AGrandChild/>需要render。
但在React中,即使性能优化后,<App/>与<AGrandChild/>沿途的组件也会render:
而默认情况下(不优化性能),整个应用都会render:
造成这一问题的原因在于 —— 对于任一状态,React不知道哪些组件依赖他。
在「props下钻」场景下,虽然<App/>与<AGrandChild/>沿途的组件仅仅是传递number(而不是依赖他),但React无从得知。
那如果明确的表示依赖关系,是不是能解决这个问题呢?
比如,我们不使用props,而是在<App/>定义context number,再在<AGrandChild/>中消费number:
遗憾的是,在React中context的实现也是依赖组件树的遍历(可以理解为React内部实现的「props下钻」),所以并不能解决这个问题。
Signal
解决这个问题的关键在于 —— 明确状态与组件的依赖关系。
这种建立组件与状态之间依赖关系的技术叫「响应式更新」(熟悉Vue的同学应该不陌生),也有些框架称其为Signal。
应用这种技术的框架(比如Vue、Qwik),当状态变化,只有依赖该状态的组件会更新。
总结
正是由于React底层架构的原因,导致应用的性能优化无法达到最理想的状态。
这同时也是为什么React中有很多性能优化API(比如React.memo、useMemo、 useCallback...),而采用Signal技术的框架没有这些性能优化API的原因。