Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

高性能react-formutil表单优化指南 #18

Open
qiqiboy opened this issue Mar 9, 2020 · 0 comments
Open

高性能react-formutil表单优化指南 #18

qiqiboy opened this issue Mar 9, 2020 · 0 comments

Comments

@qiqiboy
Copy link
Owner

qiqiboy commented Mar 9, 2020

speed-formutil

高性能 react 表单优化指南

我们的页面为什么会卡顿?

首先,我们要统一一些术语和认知:

Virtual DOM 尤其是 16+以后,VDOM 是指什么,很多人理解不一致。我们这里所说的 VDOM 是指react elements组成的树状结构对象,其映射了真实 DOM 的结构。react element是指React.createElement创建的对象。我们平常用 jsx 语法生命的每个标签都是一个个 VDOM Node。有些人和文章会混淆 Virtual DOM 是指 Fiber Tree,这里我们不采用这种说法。官方文档的Virtual DOM and Internals对于 react 体系中的Virtual DOM下了个定义In React world, the term “virtual DOM” is usually associated with React elements since they are the objects representing the user interface.。不过呢,我个人认为把 Fiber Tree 当作 Virtual DOM 也没什么问题,本身 FiberNode 就是对 elements 的升级补充。

Fiber 是指 16+以后新加入的主要针对reconciliation过程的核心算法的深度优化重写后的技术的泛称,是个抽象的概念。完整名称叫 Fiber reconciler

Fiber Node Fiber Node 是 Fiber 算法中的最小工作单元,其是用来描述 Fiber 要进行的工作的数据结构。它可以认为是对 react element 进行额外信息补充的特定结构对象,其可以认为是对 react element 的升级。react 16+以后 react element 结构简化了,就是因为更多的信息转移到了与其一一对应的 Fiber Node 中(_owner 属性)。另外 FIber Node 不会每次都重建,它是可变结构,保存了对应 react element 的相关组件信息

Fiber Tree 即由Fiber Node组成的结构,每个 Node 都通过return child sibling链接父、子、兄节点,组成一个大的链表式结构。Diffing 算法基于 Fiber Tree 进行

Reconciliation 协调,是指利用算法 diff 两棵 Fiber Tree 之间的差异,来决定需要更新的部分。具体大概包括生成 element、生成/更新 Fiber Node、调用 render、调用 life cycles、diff 等,其不包括后续renderer(commit)阶段。我们下面所说的“组件重新渲染(时间)”就是指进行reconciliation阶段。但是请注意,通常意义上的所说的渲染应当是包含 reconciliation 和 renderer(commit)两个阶段。

render/commitreconciliation/renderder通常可以认为是等价的,都是指 react 一次完整渲染的两个阶段。我们后面主要用commit来指代第二个阶段,提交变动到 DOM/渲染器进行 UI 更新

props、children 请注意,children是属于 props 的,事实上,<div>123</div>等同于<div children="123" />,优先级上,前者大于后者;我们所说的对组件 props 做比较,默认都是包含props.children

children 再说children属性,在component/react element层面指其props.children,但是在Virtual DOM/Fiber
Tree中,则是指其子节点。props.children不一定是子节点,因为组件本身可能不渲染props.children或者还会额外渲染其它elements。children属性和children节点不一定是相同的。

标准测试用例 后面在表单优化环节,会提到这个,这是指react-antd-formutil的 demo,其基于 antd 框架,包含约 30 个所有的 antd 中的data-entry型组件所组成的一个中型 form 表单。我们所说的所有的优化前后对比结果也是基于这个测试用例所述

在浏览器层面,卡顿无非就是 js 线程卡或者 UI 绘制线程(DOM 更新)卡。

具体到 react 应用中,绝大多数情况,我们的组件树结构没有大的变化(即不会产生大的 DOM 变动),但是页面依然出现了卡顿,那么就是只有一个原因:组件的reconciliation过程开销巨大。

我们这里主要讨论 reconciliation 阶段的优化,DOM 频繁更新(commit)导致的卡顿不在这个范围内,后者主要是保证生成 DOM 的稳定。

具体原因就是由于组件的 state 或者 props 变化,react 会从当前组件进行reconcilier,开始重建整个 react elements。大量的重建 elements、Fibe Node、Diff 消耗了大量的计算和资源。当 js 线程占用过久,就会影响浏览器渲染,进而引起页面掉帧,用户开始感觉到卡顿。

个人补充:一般来说,重建 element 是会很快的,因为就是创建一个个 js 对象。Diffing 一般来说也没啥太大问题,作为核心算法,不会是 react 的瓶颈。关键就是 element 合并生成/更新 Fiber Node 时,如果 render 方法开销较大,就会极大影响 reconciliation 的性能;当然如要更新的组件树过于庞大,即使每个 element 渲染只需要 0.01ms,上万的节点处理依然需要每次 100ms+的耗时,这也会导致卡顿

所以关键就是减少组件重新执行 render,也就得减少组件进入 Reconciliation。

如何优化 react 组件?

知道了性能问题产生在哪里,那么就减少导致性能的情况出现即可,也即 Avoid Reconciliation。我们要避免reconciliation开销大的组件非必要重复渲染。

在 react 中,可以阻止组件渲染的方法就是重写 class 组件的shouldComponentUpdate生命周期方法,或者通过 React.memo 优化 fuction 组件。React.PureComponentshouldComponentUpdate浅比较 props 和 state 的快速实现。

但是注意,shouldComponentUpdate 和 React.memo 的返回值是不一样的,shouldComponentUpdate 如果 props 和 state 一致,不需要重新渲染,要返回false;而memo则正好相反, true表示不需要渲染,反而false是可以渲染。另外memo无法比较 state,因为函数组件外部无法获取内部 state。不过幸好,useState对于更新相同的 state 并不会触发rerender

class App extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        if (isEqual(nextProps, this.props)) {
            return false;
        }

        return true;
    }

    render() {
        return null;
    }
}
/////////////////////////////
const App = React.memo(
    () => null,
    (prev, next) => {
        if (isEqual(prev, next)) {
            return true;
        }

        return false;
    }
);

借图说话:

The last interesting case is C8. React had to render this component, but since the React elements it returned were equal to the previously rendered ones, it didn’t have to update the DOM.

Note that React only had to do DOM mutations for C6, which was inevitable. For C8, it bailed out by comparing the rendered React elements, and for C2’s subtree and C7, it didn’t even have to compare the elements as we bailed out on shouldComponentUpdate, and render was not called.

这里 C8 有个疑问,官方文档上对这个图的解释是,C8 生成的 react elements 和之前对比一样,所以不需要更新 DOM。但是查了很多文档,reconciliation就是指创建 elements、render、diff 这些过程,那么按说 C8 算是进入 reconciliation 了,但是图上将其标记为绿色。这里绿色圆圈应该是指是否需要更新 DOM?。

常见问题

PureComponent 这么方便,我是不是应该编写组件时都直接使用 PureComponent 就好了?

这个答案一定是否定的。react 没有把基于 shouldComponentUpdate 的浅比较作为默认的组件更新行为就知道,这么做一定是不合适的。它会导致许多问题变得复杂,具体原因如下:

  • 常见的内联的 object、function 类型的 props,会总是破坏 PureComponent 的浅比较。这会导致额外的比较反而成为额外的开销。当成千上万的 PureComponent 都因为类似原因导致重复比较,那么带来的性能损耗可能会显而易见
  • 组件依赖的上层数据,即 props 可能是复杂的,其不一定是Immutable的,这会成为潜在的 bug 点,并难以被发现。对于复杂的数据结构,如果没有应用Immutiable技术,那么对其子属性的更改可能无法生成可变值;或者在传递前,总是简单的{...xxx},那么就会导致上一个原因,即 prop 总是在变
  • 为了避免前面的问题,我们需要特别注意组件 props 的值的声明位置和方式,这会导致代码逻辑组织的困难和复杂。这一点造成的问题,可能远比 PureComponent 带来的改善严重的多
  • 导致 legacy context 的更新失效 当然,新的 context api 让这一点不再成为问题
  • 在团队项目中,由于能力经验差异,能力相对较低的人由于对 react 渲染机制的陌生,很容易写出破坏辛苦维护的优化结果的代码,单点破坏,可能就会造成严重影响。当然,这一点对于老鸟,如果疏忽也会有这个问题。(这一点是指本来组件树没有大的性能问题,但是团队习惯于都是使用 PureComponent,但是容易由于疏忽或者 review 漏掉,导致一些造成优化失效)

PureComponent 容易被可变对象破坏,那我用 shouldComponentUpdate 深比较可以了吧?

这个答案更是否定的。深比较虽然可以解决 object 对象的一致性比较,但是也存在非常明显的问题:

  • 大数据的比较可能带来相当大的开销,这极有可能比让组件重新渲染大得多。大数据可能来自与单个 prop 值,也可能是组件的 props 数量过多,加起来造成需要 diff 的值偏大
  • 可变函数(inline render props)值依然无法深度比较,这同样会导致负向优化

我自己根据实际组件编写 shouldComponentUpdate 比较逻辑总可以了吧?

可以这么做,但是一定要非常小心。因为没有一成不变的组件,未来在业务发展、项目迭代中,可能会因为新加入的 props 不符合 shouldComponentUpdate 的比较逻辑导致组件出现渲染异常。并且这个异常对部分人来说可能是难以察觉、发现的。如果对于 react 的更新渲染机理非常熟悉,可以编写良好的比较逻辑,当然这么做是个好的选择

基于以上考虑,我给的最佳实践方案是:

避免提前优化、过度优化

  • 如果我们的组件没有发生性能问题,就不要提前使用任何PureComponent memo shouldComponentUpdate等技术
  • 如果发生性能问题,优先考虑优化组件的划分、组合、状态管理等逻辑,尽量在组件使用管理上避免开销大的组件频繁被渲染;千万不要组件一遇到性能问题,就一刀切想着要上 SCU
  • 如果第二条无法做到,再考虑使用渲染优化技术介入,并且要克制的使用。任何的渲染优化,都在破坏 react 本身默认的组件树渲染逻辑

使用了 shouldComponentUpdate 阻止了组件渲染,是否就一定阻止了所有子组件的渲染?

并不是这样的,新的 context 不受这种优化的影响。即如果 context.Provider 传递的 value 发生了变化,所有连接的 Consumers,包括使用 useContextHook 的函数组件,都会重新渲染,不受上层组件的 shouldComponentUpdate 影响。这个特性也正是相比于老版本 context 的改进之一。

但是这个特性不注意也会导致 shouldComponentUpdate 优化失败,导致依然有性能问题产生:

const themeContext = React.createContext({});

function App() {
    const { Provider } = themeContext;
    return (
        <Provider value={{ color: 'white' }}>
            <PreventRender />
        </Provider>
    );
}

const PreventRender = React.memo(
    () => <ColorConsumer />,
    () => true
);

function ColorConsumer() {
    const { color } = React.useContext(themeContext);

    return <div>{color}</div>;
}

这个例子中,如果 App 重新渲染,即使 PreventRender 使用了 memo 阻止了重新渲染。但是 ColorConsumer 组件依然会被重新渲染。

总结

  • 按需优化,避免提前优化、过度优化
  • 优先通过调整组件的状态管理、划分、复用等逻辑,将引起组件频繁渲染的范围降低到最小以节省开销
  • 善于使用 chroem 的 Devtool 的 Performance 或者 React Devtool Profiler 来发现、定位问题组件

对于第三方组件或者不方便直接进行渲染优化改造的组件,可以尝试memo-render这个组件,它可以方便的在无需调整组件内部逻辑的情况下达到优化组件树渲染的目的。我们在下方的表单优化环节也会对此进行介绍。

react-formutil 1.0 是如何优化的?

之前版本的 react-formutil 的性能瓶颈在哪里?

前面我们讲到了 react 性能降低来自于复杂组件的reconciliaton的开销。而react-formutil的 Form 作为全局状态控制器,同步所有 Field 的状态更新。在表单场景中,用户快速输入导致的整个 Form 频繁进入reconciliation。如果 Form 中存在一个明显reconciliation开销过大的组件(不一定需要是 Field 相关组件),那么就会导致页面出现明显卡顿。具体来说react-formutil的性能问题主要来自以下两点:

  • 实时表单状态同步的设计理念下,任何 Field 的变动都会导致 Form 整体渲染;而 Form 中又存在reconciliation开销过大的组件,就导致了性能问题
  • Form 组件本身的性能,其在 render 时实时生成$formutil对象,该对象是Immutable的;而$formutil是一个非常复杂的对象集合,每次渲染重新生成,需要从每个 Field 中提取状态,进行计算合并。并且由于要支持nested path name,这个计算要考虑的情况很多,导致计算生成对象也会花费较多时间。

分布式表单可以高效,为什么不用分布式设计?

这里首先对分布式表单和 react-formutil 所代表的集中式/全局管理表单做个对比:

性能上

毫无疑问,分布式表单完胜全局表单,因为分布式表单的 Field 状态各自管理,当 Field 变动时只更新自身,不影响其它 Field 或者 Form 下的其它组件。而全局表单,单个 Field 变动会造成整个 Form 的重新渲染。

易用性

这一点,毫无疑问全局表单完胜分布式,因为 react 本身的特点就是自上而下单向数据流。集中式表单的状态传递就很好的契合了这一点。使用全局表单,在表单的 context 中,访问表单状态非常自然,就从读取父级 props 中传递的值即可。任何 Field、非 Field 组件都能只有读取表单状态。

而在分布式表单中,访问表单状态或者其它 Field 成为了一件略显棘手的事情。首先是需要触发动作,例如访问表单的值对象,需要手动触发getFormValues();获取其它 Field 的值,需要getFieldValue(name);另外由于只有变动的 Field 才会更新渲染,所以为了让其它依赖的 Field 可以更新,还需要明确指定相互间的依赖,或者使用类似sub/pub的状态订阅分发设计模式。另外对于非 Field 组件要获取表单的 Field 值,也会很困难。在这些场景中,一般需要通过onFormChange/onFieldChange把这些值更新到上层的 state 中,然后供其它组件访问。但是这也带来了重复渲染的问题,额外的 setState 造成不必要的整树更新。类似场景增多的话,同样带来性能隐患。

在表单中,react-formutil 认为副作用访问是个常见需求,即可能随时、到处存在需要访问 Form 状态的组件,我们优先保证在这些需求,最小代价满足。而分布式表单,明显是假设表单中不存在副作用,当有副作用需要时,手动去管理。

总结

  • 分布式表单性能高,但是部分场景让问题变得复杂、可用性低、心智负担大,额外创造了不符合 reactive 的场景
  • 全局表单性能一般,但是易用性高、更符合 react 的使用直觉

1.0以后的版本如何解决上述问题

优化全量渲染

对于表单整体刷新,这个设计理念不会改变。既然 Form 整体的渲染不可避免,那么我们就从 Field 去优化即可。Form 重新渲染时,如果当前 Field 不需要更新,那么就阻止掉该次渲染。

但是为了向后兼容性,默认情况下,Field 和之前一样,跟随 Form 重新渲染。所以我们新增了$memo属性,用来表明当前 Field 是否进行状态比对。

优化$formutil生成

$formutil虽然每次渲染重新生成,但是导致渲染的往往都是个别的 Field 的变化。所以我们只要记录下导致本次渲染发生的 Field,然后只重新生成该 Field 在$formutil中的状态集合即可。

所以1.0版本,开始,$formutil会浅拷贝之前的$formutil上那些不变的值,只重新计算发生变化的 Field。但是有个例外,即 Field 有unmount 发生,即有 Field 被移除注册(包括当前 Field 的 name 值发生变化,这会同步触发一次 unmount/mount),$formutil还是会进行全量计算,这是因为从庞大的$formutil中计算级连移除值时的计算开销并不一定比全量重新生成的开销小,尤其是计算如何移除nest path;而且可以避免特殊的例如 Field 具有undefined值时如果进行深层对象清理产生一些 bug(undefined是目前清理算法中的待移除标记)。

幸好unmount的发生不会是高频场景,这种情况即使发生了导致$formutil全量重建,也是可以接受的。

在我们的标准测试例子中,Form 的 rerender 性能从之前的均30ms上下降低到<1ms,几乎与普通轻量组件无异。

如何使用 react-formutil 进行高性能表单开发?

1.0 的react-formutil对于$formutil的计算优化是自动的,这一点无需用户关心。要创建高性能表单,主要是通过降低 Form 下的高开销组件的rerender来优化。

但是在进行优化前,还是要说一句:

当 Form 出现性能问题后再进行优化,避免提前优化、过度优化!

这与之前提到的 react 组件进行渲染优化可能导致的潜在问题原因一致。在实际业务场景中,Field 的各种属性,包括$validators、$parser 或者 children 等都可能依赖各种上层状态,贸然进行渲染优化,可能导致负向优化产生,或者导致难以察觉的问题。

react-formutil的设计就是全局表单状态同步,方便在 Form context 中,随时随地可以访问整个 Form 的状态。这是很自然、很 reactive、易用的,在没有明显性能问题前,保持表单组件按照 react 本身的reconciliation逻辑进行更新,避免潜在问题。

发现问题所在

当表单出现性能下降的,首先要找出reconciliation开销大的组件。可以通过 react 的Profiler相关工具测试组件的渲染开销。要说明的是,

优化开销大的非Field组件

开销大的组件不一定是 Field 组件!

例如如下场景:

// 待优化例子,Table为低性能组件
<Form>
    <Field name="page" />
    <Field name="search" />
    <Table page={$formutil.$parmas.page} />
</Form>

如果页面卡顿,可能是因为 Table 组件放到了 Form 下,其本身的reconciliation如果开销较大,那么就会导致 Form 更新时,其成为渲染瓶颈。

针对这种情况,有以下两种处理:

将 Table 移出 Form 组件

这样之做避免 Table 被 Form 的频繁渲染影响。如果 Table 需要访问表单值,可以通过 Form 的$onFormChange把值传递到上层组件后传给 Table。

// 优化后
function App() {
    const [page, setPage] = useState(1);
    const onFormChange = React.useCallback($formutil => {
        // useState同样的value不会触发重复渲染,所以可以放心直接进行setState操作
        //  而不用担心导致App组件随同Form的每次change都重新渲染
        setPage($formutil.$params.page);
    }, []);

    return (
        <div>
            <Form $onFormChange={onFormChange}>
                <Field name="page" />
                <Field name="search" />
            </Form>

            <Table page={page} />
        </div>
    );
}

优化调整 Table 本身的组件渲染

如果 Table 组件是自有组件,可以在 Table 加入 shouldComponentUpdate 渲染优化。但是如果 Table 属于第三方组件,那么这一条就不成立了,请看下一条。

通过第三方组件拦截 Table 的渲染

这个适用于要优化的组件是第三方组件,无法直接改造优化,或者不想在组件加入影响渲染机制的优化,只是想在此处临时处理。

这个就会用memo-render这个组件:

// 优化后
<Form>
    <Field name="page" />
    <Field name="search" />
    <MemoRender>
        <Table page={$formutil.$parmas.page} />
    </MemoRender>
    {/* 或者 */}
    <MemoRender deps={[$formutil.$parmas.page]}>
        <Table page={$formutil.$parmas.page} />
    </MemoRender>
</Form>

优化开销大的Field组件

对于 Field 的优化,则是通过$memo属性。事实上,它与memo-render是相同的优化原理。

interface FieldProps {
    $memo: boolean | any[];
}

$memo可以传递一个布尔值或者一个数组。

  • 当传递数组时,它与useCallback useMemo的第二个deps参数类似,即传入的数组中的值作为渲染比较的依赖项
  • 当传递空数组时$memo={[]},表示除了 Field 本身的状态变化,阻止所有重复渲染
  • 当传递true时,表示深度比较 Field 的所有 props 是否一致来决定是否重新渲染

如果你比较了解了 react 的渲染优化控制,那么可以根据实际情况选择怎么使用$memo。当然,大多数情况下,显式地指定$memo一个比较依赖数组无疑是最高效的选择。

$memo并不要求所有的 props 属性都是不可变值,可以传入 object 等类型的值,它会使用deep diff技术。但是我们要指出一些容易进入陷阱的误区:

Field 具备可变函数或者包含可变函数的属性

以下a b c三个 Field 都属于具有可变函数属性(inline-render-props)。c中的$validators虽然是个 object,但是$validators.required是个函数,深度比较时依然会导致比较失败。

// 错误示范
<Field $memo name="a" $parser={value => value.trim()} />
<Field $memo name="b">
    {$fieldutil => {/*...*/}}
</Field>
<Field $memo name="a" $validators={{
    required: value => !!value || 'Reuqired!'
}} />

如果要优化的话,就是把这些可变函数值变为不可变值。即如果是 class 组件,就放到组件实例上,如果是 function 组件,使用useCallback优化:

// 优化后
function App() {
    const $parser = React.useCallback(value => value.trim(), []);
    const renderFieldB = React.useCallback($fieldutil => {
        /*...*/
    }, []);
    const $validators = React.useMemo(
        () => ({
            required: value => !!value || 'Reuqired!'
        }),
        []
    );
    return (
        <Form>
            <Field $memo name="a" $parser={$parser} />
            <Field $memo name="b">
                {renderFieldB}
            </Field>
            <Field $memo name="a" $validators={$validators} />
        </Form>
    );
}

更高效的优化是,如果明确知道这些函数的渲染依赖项或者没有任何依赖,可以明确指定依赖比较项:

//  优化后
<Field $memo name={[]} $parser={value => value.trim()} />
<Field $memo name={[]}>
    {$fieldutil => {/*...*/}}
</Field>
<Field $memo name={[]} $validators={{
    required: value => !!value || 'Reuqired!'
}} />

因为这里三个 Field 的函数属性都不依赖第三方状态值,所以直接设置空数组即可。但是假如或有依赖呢?

如下的例子,这里直接$memo={[]}将导致,即使当前组件的props.isTrmValue变了,但是 Field 依然不会更新。导致用户此后进行输入的第一个字符可能无法正确被处理。

要优化这个问题,可以将props.isTrmValue假如$memo数组即可!则是 Field 就知道,如果props.isTrmValue值变了,需要重新渲染自身。

// 错误示范
<Field $memo={[]} name="a" $parser={value => props.isTrimValue ? value.trim() : value} />

// 优化后
<Field $memo={[props.isTrimValue]} name="a" $parser={value => props.isTrimValue ? value.trim() : value} />

Field 包含大数据值属性

大数据值是指数据量特别大,例如一些富文本编辑器,例如draftjscontentState对象,就是非常复杂、庞大的数据。使用$memo进行深比较的话,器带来的开销很有可能会大于对这个 Field 进行重复渲染的开销!所以需要一些特殊手段。

下面的例子中,由于 Editor 组件的 contentState 是个大数据值,所以无论$memo={true}还是$memo={[contentState]}都是不够高效的,但是我们可以基于 contentState 的浅比较创建一个随着 contentState 变化而变化的值,作为$memo比较的依赖项:

// 优化前,直接传递contentState到$memo,深比较性能较差
<Field name="editor" $memo={[contentState]}>
    <Editor contentState={contentState} />
</Field>
// 优化后,hooks示例
function App(props) {
    // 使用useRef存储
    // 注意不要使用useState等存储,否则setState会带来额外的渲染
    const contentStateRef = useRef({
        contentState: props.contextState,
        renderCount: 0
    });

    useEffect(() => {
        // 记录每次渲染后的contentState
        contetnStateRef.current.contentState = contentState;
    }, [props.contentSatte]);

    // 当前contentState与前一次的浅比较不一致后,将renderCount+1
    if (contetnStateRef.current.contentState !== props.contentState) {
        contetnStateRef.current.renderCount++;
    }

    return (
        <Form>
            <Field name="editor" $memo={[contetnStateRef.current.renderCount]}>
                <Editor contentState={props.contentState} />
            </Field>
        </Form>
    );
}

// class示例
class App extends React.Component {
    state = {
        renderCount: 0,
        contentState: this.props.contentState
    };

    static getDerivedStateFromProps(props, state) {
        if (props.contentState !== state.contentState) {
            return {
                state: state.renderCount + 1,
                contextState: props.contextState
            };
        }

        return null;
    }

    render() {
        return (
            <Form>
                <Field name="editor" $memo={[this.state.renderCount]}>
                    <Editor contentState={this.state.contentState} />
                </Field>
            </Form>
        );
    }
}

总结

  • 最小化原则,即将 Form 放到离 Field 最近的地方,避免直接套在一个过高的顶层组件树中
  • 被动优化原则,即当发生了渲染问题后再进行优化,避免提前优化、过度优化
  • 快准狠原则,即分析找出影响渲染的组件,精准优化;避免不成为性能瓶颈的 Field 也被优化
@qiqiboy qiqiboy pinned this issue Mar 9, 2020
@qiqiboy qiqiboy changed the title react-formutil表单性能优化指南 高性能react-formutil表单优化指南 Mar 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant