摘要
随着es2015、babel等技术在前端迅速普及,前端开发效率大幅提升。vue、react之类的框架也被广泛采用。在使用基于react的antd构建后台系统的过程中遇到了明显的性能问题。在上期文章发表后收到了许多赞。随着使用场景的变化,又有许多性能问题暴露出来。因此继续研究antd的性能问题,寻找最终解决方案。
antd简介
antd是由阿里旗下的 蚂蚁金服体验技术部 开发的一套UI设计语言,包括了设计样式、规范、组件库等内容。antd基于React开发,并且是起步较早的框架,因此功能上比较完善,使用者较多,社区资源也多。因此在去年技术选型中选中了antd作为DA后台系统前端的主要框架之一。
性能问题
所有的框架在降低开发成本的同时必然会牺牲一部分的性能,antd 也不例外。虽然 antd 基于的 React 生态一直在积极的优化性能,然而在极端情况下依然会出现性能问题。
名词解释
受控状态 / 非受控状态
有交互的 React 组件通常都会有成对的控制参数和回调方法,例如 Input 组件里的 value 和 onChange 。当使用组件的开发者设置了 value 时组件就处于“受控”状态( Controlled ),反之不设置 value 组件就处于“不受控”状态( Uncontrolled )。受控状态的组件在发生交互的时候并不会直接更新自己的状态,而是将变化的值返回给父组件处理,再经过父组件设置 value,受控组件才会发生相应变化。
受控模式:
非受控模式:
debounce
防抖动(Debounce)和节流阀(Throttle)在 javascript 中已经是广泛应用的方法,主要用于用户操作实时提交到服务器时限制提交频率。 Debounce 会在最后一次触发的一段时间后开始执行,Throttle 则是会在上一次执行一段时间后才会开始下一次执行。在它们两次执行间的触发事件则会被忽略。
问题分析
上一次的方案对付性能问题,得到了不错的效果。不过使用复杂组件时依然需要定制组件,所以笔者就寻找更方便的方法。
在处理 Form 表单过大导致 Input 输入框卡顿的问题上,上次的做法相当于将 Input 输入框和其他复杂组件隔离以避免用户输入时感到卡顿。受此启发,如果将 Input 输入框与 Form 隔离,则用户输入啥都不会感到卡顿了。
在使用复杂组件(例如 antd 的 Tree ,加载了数万个节点)时,开发者通常情况下并不能修改组件本身的代码。这种组件本身的性能就很拙计。不过细心的开发者会发现 antd 的 Tree 在受控状态和非受控状态性能差距比较明显。这是因为非受控状态下组件本身可以使用方便计算的格式保存结果,受控状态下则必须在每次变化时转换为输出格式,在控制变量变化时再转换为内部计算格式。如果转换过程比较耗时就会感到卡顿。
1.选择合适的数据格式转换时机
后端返回的数据有时候并不符合端的组件要求。 在大多数例子中会在Render里做数据转换,这也是可读性最高的形式,也是性能最差的形式。 遇到性能间题时,官方提示使用 sholdCompomenmUpdate ,但是 sholdCompomenmUpdate 通常是比较 nextProps 和 this.props 是否变化而控制更新。
如果 redux 中存储了一个用字符串表示的日期参数,在父组件的 render 里转换成 moment 传入日期控件,那父组件每次执行 render 后都会生成一个新的 moment 传入子组件中,子组件每次都会得到一个新的对象,导致 sholdCompomenmUpdate 无法按预期的生效。
那能否在这个参数变化时才进行转换,而其他时候传入的是一个缓存呢?答案是可以。在父组件的 constructor 和 componentWillReceiveProps 里进行格式转换并缓存在 state 里,再将 state 里的结果传入子组件中。当这个参数变化时就会执行转换,而其他时候子组件会得到同一个转换结果,所以子组件的 sholdCompomenmUpdate 可以正常工作。
(例子) 使用生命周期 + state 缓存参数虽然可以提高性能,但是会降低代码可读性。因此应该根据页面的复杂度选择使用。
2.使用高阶组件为现有组件增加 debounce
首先我们来看看 React 的生命周期。如果很熟悉可以跳过下面一段话。
React 生命周期包括 constructor、componentWillMount、componentDidMount、shouldComponentUpdate、componentWillReceiveProps、render、componentWillUpdate、componentDidUpdate、render、componentWillUnmount。关于它们的含义,可以看 《React组件生命周期小结》
点我查看 示例页面
从父组件触发form更新,主要是从服务端获取数据填充到表单里触发更新,可以看见整个表单的组件都更新了。
从表单域触发form更新,主要是用户修改表单内容触发更新。整个表单又更新了。
可以看见在这两种主要场景中都会触发整个组件的更新。特别是用户在输入框中输入的时候,每输入一次就会执行这么多的流程,这包括了表单和表单所有子组件的更新。那为什么不能只更新用户输入的表单域组件呢?这是 React 的模式和 antd 的实现造成的。
如果不使用 antd 的 Form 而表单直接保存到 state 中,是这样的
使用了 antd 的 Form 后,是这样的
因为 antd 将表单数据和处理逻辑都放在父组件里,并且表单组件有许多联动功能,再加上 React 里避免使用 shouldComponentUpdate 的原则,造成了这样的结果。 很多表单域组件,拥有一对 value 和 onChange 控制组件状态、获取结果。特别是表单中用到的组件,几乎都是这种类型。但是当表单中包含很多表单域组件的时候就会遇到性能问题。
受到在实时搜索中使用 debounce 的启发,我想到用户输入时先将表单域组件更新,延迟一段时间后再触发 onChange 事件更新表单中的数据,这样就能立刻响应用户输入,对原有的组件组合方式也不会造成很大变化。
而且当组件发现 onChange 输出的数据和接下来传入 value 的数据一样,则不会触发更新,这样就能避免子组件的二次更新。 先进行效果测试,对比的是使用 debounce 高阶组件之前和之后的效果
使用前
使用后
测试地址:assweeecan/react_component_debounce_test
测试页面:assweeecan/react_component_debounce_test
可以看见使用前,用户输入很卡顿,体验极差。使用后,用户输入会立刻渲染出来,然后才会更新表单中的数据,用户体验有着质的飞跃。
并且使用后 checkbox 的选中和删除速度都大幅度提高。因为使用前会等待整个 group 生命周期执行过去才会显示 checkbox 状态改变,而对每个 checkbox 使用后则会在用户点击时立刻显示变化,再更新 group 里的数据。
要实现这个功能,就要介入表单和表单域的生命周期,在 componentWillReceiveProps 和 onChange 事件中做处理。笔者参考了 react-redux 的实现方式,就是在表单域组件外面套一层高阶组件再放入表单中,就可以介入它们之间。
import reactComponentDebounce from ‘react-component-debounce’;
import { Checkbox } from ‘antd’;
const CheckboxA = reactFormFieldDebounce({
valuePropName: ‘checked’,
triggerMs: 250,
})(Checkbox);
当高阶组件收到 value 的变化,会延迟一小段时间才将 value 保存到自己的 state 中,再将 state 中的 value 传到子组件中,实现了延迟设置 value。
再加上 shouldComponentUpdate 的控制,就能有效减少输入框、选择框等等表单域组件的更新次数,提高流畅度。 不过,使用 debounce 高阶组件也是有限制的,必须是 onChange / value 成对出现的, onChange 传出的值与 value 传入的值一样的,onChange 没有过滤 value 值的组件才能使用。否则就只能使用单纯的 shouldComponentUpdate 控制更新了。
再对组件增加一些附属功能,例如自定义 value、onChange 参数名称、自定义延迟时间等等,就完成了这个高阶组件。
原文链接
https://zhuanlan.zhihu.com/p/28980381