React中一次性能优化之旅
技术栈提示
- 背景中所描述的场景使用的技术栈为:
React + antd + umi生态
, 只说明一次,后文中不再出现 - 文中所描述的一些示例地址在github (opens new window),如果对示例感兴趣的同学可以动动手指点个start
# 背景
在一次业务开发中需要使用表格来编辑一些数据,于是乎便用了antd
周边生态的ProTable
,ProTable
中有提供EditProTable
的组件和示例代码。EditProTable
是针对表格编辑的场景下封装的编辑表格组件,如果在编辑数据不需要受控的情况下,那么EditProTable
是没有问题的。如果在数据需要受控,并根据编辑的数据信息去额外计算一些数据渲染到表格的某一行某一列中,那么EditProTable
提供的编辑组件,在用户的角度来使用是及其卡顿的。数据量稍微大一些的情况下都有可能直接渲染崩溃。为什么EditProTable
在数据受控模式时会这么卡顿呢?遇到这种情况,应该怎么去优化性能?让我们带着疑问去阅读这篇React性能优化之旅的文章
# React中的性能优化该怎么优化?
可能有些小伙伴有阅读过一些react
性能优化的文章或者指南,通常这些文章会占用极大的篇幅来介绍React
底层原理,最后几句话来概括react
性能优化的方式。了解原理是很重要的,但是我比较直接,所以我直接用一句话来概括react
的性能优化。那么这句话则是: 尽可能的减少render
渲染
尽可能的减少render渲染,是因为react
、vue
等框架都是数据驱动视图更新。那么如果在一个有几百上千的列表视图下,列表中的某一行的数据更新了,是一次性重新渲染这几百上千条列表数据?还是只单独更新数据改变这一行DOM
节点?答案显而易见,当然会选择只更新一行DOM
节点的方式。
可惜,antd
周边生态提供的Table、EditProTable
组件都是一次性重新渲染表格。
这里稍微提一嘴,如果单独去对比React
框架和vue
框架的性能,那么Vue
的性能是好于react
的。其原因是vue
使用的<Template>
开发方式,在vue编译的过程中是可以对<Template>
做很多性能优化的判断。而react
的性能表现之所以不如vue
,是因为react
的JSX
开发方式太灵活了,很难去针对JSX
做单独的性能优化判断。react
不是没有尝试去优化过框架的性能表现,但最终是没有对JSX
做性能优化,而是提出了Fiber
概念来优化框架的性能表现。Fiber
的原理一句话概括: 将JS的长任务切割为一个个微任务,在浏览器FPS的时候去执行微任务。解决了并不会因为该js长任务执行时间过长而阻塞后续任务
当然,这篇文章并不是在探讨react
源码、vue
源码。而是记录一下在react
中优化性能的旅途,所以收回正题。
# Table、EditProTable组件的原罪
首先,antd
的Table
组件犯了懒惰之罪。这个问题不是没有抛出来,而是抛出来,但是官方没有针对表格编辑的场景下去优化这个问题。antd
团队提供的EditProTable
还是存在这个问题... 那就只能证明他们是真的懒
# 具体的问题
我们使用浏览器的性能分析页面可以得知导致卡顿的具体原因,下图是用Table
表格在编辑场景下的性能分析图
在图上可以看到三块黄色的区域,黄色表示的是
js
执行时间。那么最显著的问题是js
执行太长了,只改了一个输入框的内容,就要执行1s秒左右的js
逻辑。
而导致js
执行这么长时间的问题,则是table
组件只要检测到数据源列表有更新,就会去重新渲染表格。而表格的cloumns中的render
方法都会重新执行一次。因为table
的每一行(row)的js
计算逻辑是DataSource.map((row) => columns.map((column) => Column_ReactNode))
。所需要的时间就是2n(o)
所以,当数据量只有十几条、几十条的时候并不会感受到明显卡顿,因为渲染十几条数据并不会需要太 长时间,如果,数据多起来了呢?几百条数据、几千条数据呢?这种大数据的情况下,所有数据重新渲染,那么渲染成本是极高的。正确的方式是哪一条数据更改了,就只渲染这一条数据对应的DOM
节点就可以了。如果只渲染对应更改的某一条数据,那么所需时间就是n(o)
# 解决过程
具体场景为:数据需要受控,我需要根据用户的输入信息来计算一些差值用于显示在表格的某一行某一列中。
当在测试环境中,由大量编辑数据触发这个性能问题的时候。想到的第一个方案便是Input
输入组件上的onChange
事件去掉。这样就避免了用户每输入一个字符,就需要重新渲染这几百条编辑数据的逻辑。
# 临上线前的妥协方案
如果去掉了onChange
时间,那么用户的编辑数据我就接收不到了,该怎么办?由于上线时间点临近,只能和产品沟通先选择一种妥协方案,那就是当Input
组件失去焦点的时候再去计算相关信息。
这样虽然解决了输入过程中的卡顿问题,但是用户如果去下一行进行编辑在短时间内是无法响应用户的交互的。其原因是表格组件重新渲染了呀,而且渲染所需要的时间又比较长,用户大概等1s~2s(具体时间视数据量而定)。
所以这个问题最终只解决了一半,并没有完全解决table
组件重新渲染所有行的情况。
# 最终的解决方式
对于追求丝滑的web交互的开发者来说,这种卡顿的情况是决不允许的。所以,自己便在每天晚上下班的时候,针对表格编辑的场景重新实现了一个EditTable
。自己实现的版本则是某一条数据更新那么就只会渲染对应的这条数据的DOM
节点。
其核心代码如下
// 定义每一行(row)的刷新方法
const [, setRefresh] = useState<string>();
const onRenderChange = useCallback(
(key: string, event: any, renderDom) => {
// 如果有外部传入Input等组件,那么则调用外部Input组件上定义的onChange方法
if (!isArray(renderDom) && renderDom.props && isFunction(renderDom.props.onChange)) {
renderDom.props.onChange(event);
return;
}
if (isObject(event) && (event as any).target) {
const {
target: { value },
} = event as any;
Object.assign(record, {
[key]: value,
});
} else {
Object.assign(record, {
[key]: event,
});
}
// 触发每一个行(row)的刷新
onRefresh();
},
[onRefresh, record],
);
return (
// 省略jsx逻辑
<>
{item.render(
record && record[dataIndex || ''],
record!,
// 将每一行(row)定义的刷新方法抛给外部使用
{ onRefresh },
)}
<>
// 省略jsx逻辑
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
自己实现的EditTable
组件,完全解决了antd table
组件的在每次数据更新的时候重新渲染整个表格的问题。所以,无论在数据量有多少条的情况下,都不会在编辑的过程中感受到卡顿。
最后,来看下浏览器的性能面板分析
可以看到,黄色所表示的
js
执行时间基本很少了... 因为每次修改只渲染了一条数据,一行DOM
节点。完美
# 延伸
上面只是对编辑过程中的表格渲染的优化,而表格每次初始化的情况下也是存在需要占用2n(o)
的时间问题的。那么这个问题有没有解决方式呢?最常见的解决方案是虚拟滚动方案。
其虚拟滚动的实现原理则是一次性不会全部渲染所有数据到页面中,而是在可视区域内,只渲染可视区域内需要用到的数据即可。那么这种方式下,即使数据有几万条,在表格初始化的情况下,也只是渲染了可视区域内的几十条数据而已。 剩下的数据,都还没有渲染到浏览器中。
而我目前的工作中的实际业务场景中并没有多到一次性编辑几万条数据的情况,所以并没有加虚拟滚动的逻辑。加这个逻辑还是挺简单的,react
生态中有提供解决方案和NPM
包。antd
组件库的示例中也有虚拟列表的DEMO
# 勘误
写在最后的话
文中可能会有错别字、病句等。如果有发现的朋友多多提示我下。
针对antd
的table
表格暴露出来的问题,我的解决思路如上,如果有其他解决思路的同学,可以多交流、交流呀。
文中代码仓库
文中所说的Table
、EditProTable
和EditTable
的代码示例在github中 (opens new window)