探索 rc-field-form 性能的极限

1,544 阅读4分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

最近在写 Form 组件时参考了 rc-field-form 的实现。 但是发现在大数据量的场景下 rc-field-form 的性能问题仍然很严重。以下是 rc-field-form 渲染 3000 个表单项时的效果:

GIF 2023-2-26 13-03-46.gif

从上图中可以看出,在渲染了3000个 input 时,rc-field-form 在进行频繁更新时已经出现了严重的卡顿现象。再看看在经过我们一系列优化后的效果,这里是在线地址

gif

一、是 React 太慢了吗?

我们尝试运行以下代码:

code.png

GIF 2023-2-26 13-38-50.gif

从上图中可以看到,如果不考虑其他逻辑。React 在渲染 6000input 时才会出现略微的卡顿现象。

二、rc-field-form 原理

那么为什么 rc-field-form 仅仅渲染了 3000input 就会如此卡顿呢? 下面我们将简单分析一下 rc-field-form 的原理。

1、useForm

code.png

从上图可知:useForm Hook 主要是创建了一个 FormStore 对象并对外暴露了一些内部方法。

code.png

这里的方法就比较熟悉了,大部分是我们经常用到的。具体实现这里按下不表。

2、Form

code.png

由此可知 Form 主要是使用 ContextChildren 提供了一些方法与属性。

3、Field

code.png

  1. Field 组件竟然是一个 Class
  2. Field 组件主要是调用了 FieldContext 的一些方法,即 FormStore 的方法。
  3. onSoteChange 方法中根据 info.type 的不同来决定是否应该更新。

4、FormStore

code.png

所以在 Field 组件中为其 Children 提供的 onChange 事件中使用的 dispatch 方法的本质是对所有注册的 Field 调用自身的 onStoreChange 进行遍历。同时处理字段的 dependencies 逻辑

5、总结

以上,我们可以对 rc-field-form 有一个初步的认识:

  1. Form 组件使用 FieldContext.ProviderField 组件提供了方法。
  2. Field 组件使用 registerField 直接注册了 自身
  3. Field 组件对 Children 属性进行处理后添加了 value, onChange 等其他属性后返回。
  4. rc-field-form 组件更新的逻辑就是一个循环。

三、原因分析

在第二节我们分析了 rc-field-form 的基本原理。认识到了 Field 是如何更新自身的。所以我们可以知道 rc-field-form 发生卡顿的原因: dispatch 方法执行时间过长:

image.png

可以看到在渲染 3000个表单项时 dispatch 所花费的时间大致在 20+ms 左右。这显然太慢了,那么该如何优化呢?

四、尝试优化

1、Field 组件什么时候会主动 render

在开始之前我们需要重新梳理一下 Field 组件什么时候会主动更新:

  1. 字段值发生变化。
  2. shouldUpdate 属性满足要求。
  3. meta 属性发生变更。

此时我们不考虑其他情况,假设表单所有的组件都相互完全独立,那么我们的 dispatch 函数可能会变成下面的样子:

code.png

接着我们再加上 shouldUpdate 的逻辑:

code.png

可以看到我们并没有使用 onStoreChange 函数。那么要如何根据 action.type 进行不同的操作呢?

code.png

我们可以像使用 Redux 一样,直接在 dispatch 时根据类型做出相应的处理,从而减少在循环中的判断过程。

另外 Fieldmeta 属性发生变更时,字段也会重新的 render 。 我们先略过这种情况,因为这是属于字段校验的逻辑。

2、Field 组件什么时候会被动 render

Field 被动 renderrc-field-form 中只有一种可能,那就是 dependencies 属性。

本节尝试优化 dependencies 相关的逻辑,我们先观察原有的逻辑执行时间:

image.png

哪怕我们没有设置一个具有 dependencies 的表单项,这里的执行时间仍然有 6-8ms, 以下是 triggerDependenciesUpdate 大致的逻辑:

code.png

所以每次 dispatch 时都会重新收集一遍 dependencies 信息,再然后去寻找依赖字段。那么能否提前得到 dependencies2fields 呢?

其实我们可以监听 Fielddependencies 属性,然后更新的时候就不必每次重新去收集所有的依赖信息了,只有当 dependencies 改变时我们才会重新计算。

3. 其他优化

在实现了上述所有的优化项后。我们发现 rc-field-form 的表现仍然不是很理想。但已经比之前好多了:

GIF 2023-2-26 18-26-22.gif

不过距离我们的目标仍然差一截。这个可能是 onValuesChangeonFieldsChange 这两个 api 的原因。

image.png

如果表单项过多的话,我们获取 allFieldsallValues 的时间也会成倍的增加。或许我们可以考虑传一个函数而不是帮用户计算好?或者干脆删除第二个参数,反正 useForm 都提供了这个方法。我们尝试下删除第二个参数:

GIF 2023-2-26 18-34-57.gif

包括使用 React Hook 重写 Field 组件,等其他优化项均可在此处体验。在渲染了 3000 个表单项时, dispatch 一次可以在 1-1.5ms 以内完成。