目的
- 提升前端排查性能问题的能力,如何更精确的定位到问题产生的原因
问题的产生
- 前端需要展示一些数据,要求是一次查询所有列表数据,正常情况下最多几万条数据。这些数据通过一些分隔符如
.、_分隔为树。只为了更直观的显示,还要支持选中、全选某一路径下所有子节点。过程如下图将上图转化为
- 选用的是antd5的虚拟树组件以及transfer组件,写好了分隔列表为树的算法。
- 结果就是数据量大的时候,第一次加载的时候也快,但是无论是选中/取消选中树的checkbox、transfer的全选、transfer的左移、右移操作都会卡顿,最多卡个十来秒的那种,这肯定是没法接受的。
排查问题
分析问题产生的原因
- 渲染
这里我用了虚拟树,所以排除渲染这个原因
- 内存占用情况
打印浏览器当前窗口performance.memory,发现当前窗口总共占用了84M的左右的内存,远远达不到浏览器给出的4个G左右的内存上限,所以排除内存占用较多而引起卡顿的原因。
- js执行
- 排查分隔树的算法,第一次加载的时候比较快,那么分隔树的算法应该是不会慢的。在好奇心的驱使下还是要测一下,使用performance.now()来精确的显示执行时间
const time = performance.now();
xxx(); // 分隔列表生成树过程
performance.now() - time
如下图:
可以看出在5万数据时,js执行时间是200多毫秒(js遍历这么多数据还是挺快的嘛)。
- 排查antd Tree的checkbox选中逻辑。我们知道在Tree中选中一个节点要去更新其父、子节点的状态为半选、选中、非选中。但是我这儿直接使用的是tree自身的联动逻辑,要排查antd源码就比较困难了。这个时候就要用到浏览器控制台的Performance选项来查看耗时了。
- 录制transfer选中checkbox卡顿
先用performance录制了选中树根节点的过程。展开耗时最多的js选项。发现是transfer的list.js中有个getCheckStatus方法耗时比较多(至于外层为啥那么多层,可以去看看react合成事件的派发)。
有sourcemap的可以查看antd在最近一次经过babel转换前的源码。我们可以直接在浏览器端断点调试,查看在哪行执行耗时较多。最终发现原因如下图
卡顿的原因是外层是filteredItems.every循环、里层是checkedKeys.includes双重循环导致的。而key的长度也会引起includes方法执行时间变长。
显然这里是可以优化的。在这里我添加了一个groupKeysMap方法:选用的是采用一个辅助Map结构,来记录checkedKeys数组中是否存在某个key。这样在查找keys数组中是否有key的时候只需要O(1)的时间复杂度,同时也避免了key较长引起的执行时间变长的问题。
export const groupKeysMap = (keys: string[]) => {
const map: MapType = {}
keys.forEach((k, i) => {
map[k] = {k, i}
})
return map
}
接下来就是对antd5源码的修改了,不知道怎么调试修改antd源码的同学看这里。我下的是最新的antd5的代码
修改源码打包替换后或者用yarn link后,重启react项目再来看看选中根节点耗时多少
可以看到目前getCheckBox方法在耗时列表排名中已经不见踪迹了。总耗时由原来十多秒减少到不到一秒。接下来就可以依葫芦画瓢解决transfer其它卡顿问题了。
- transfer取消选中checkbox卡顿
- transfer在targetKeys数据较多时选中/取消选中checkbox卡顿
- transfer右移左移卡顿, 新增方法groupDisabledKeysMap用来统计并返回dataSource中disabled项的keyMap
export function groupDisabledKeysMap<RecordType extends any[]>(dataSource: RecordType) {
const map: MapType = {};
dataSource.forEach((d, i) => {
if (d.disabled) {
map[d.key] = {
k: d.key,
i,
};
}
});
return map;
};
至此涉及antd/transfer组件几万数据下卡顿问题就已全部解决。
优化前/后示例
结语
追求六便士的路上又多了一颗月亮😊
不足之处期待指正👏👏👏