性能优化(Performance Optimization)之antd/transfer组件几万数据下卡顿时间由十几秒减少为一秒

1,547 阅读4分钟

目的

  • 提升前端排查性能问题的能力,如何更精确的定位到问题产生的原因

问题的产生

  • 前端需要展示一些数据,要求是一次查询所有列表数据,正常情况下最多几万条数据。这些数据通过一些分隔符如._分隔为树。只为了更直观的显示,还要支持选中、全选某一路径下所有子节点。过程如下图 image.png 将上图转化为 image.png
  • 选用的是antd5的虚拟树组件以及transfer组件,写好了分隔列表为树的算法。
  • 结果就是数据量大的时候,第一次加载的时候也快,但是无论是选中/取消选中树的checkbox、transfer的全选、transfer的左移、右移操作都会卡顿,最多卡个十来秒的那种,这肯定是没法接受的。

排查问题

分析问题产生的原因
  • 渲染

这里我用了虚拟树,所以排除渲染这个原因

  • 内存占用情况

image.png 打印浏览器当前窗口performance.memory,发现当前窗口总共占用了84M的左右的内存,远远达不到浏览器给出的4个G左右的内存上限,所以排除内存占用较多而引起卡顿的原因。

  • js执行
  1. 排查分隔树的算法,第一次加载的时候比较快,那么分隔树的算法应该是不会慢的。在好奇心的驱使下还是要测一下,使用performance.now()来精确的显示执行时间
    const time = performance.now();
    xxx(); // 分隔列表生成树过程
    performance.now() - time

如下图: image.png 可以看出在5万数据时,js执行时间是200多毫秒(js遍历这么多数据还是挺快的嘛)。

  1. 排查antd Tree的checkbox选中逻辑。我们知道在Tree中选中一个节点要去更新其父、子节点的状态为半选、选中、非选中。但是我这儿直接使用的是tree自身的联动逻辑,要排查antd源码就比较困难了。这个时候就要用到浏览器控制台的Performance选项来查看耗时了。
  • 录制transfer选中checkbox卡顿

image.png

先用performance录制了选中树根节点的过程。展开耗时最多的js选项。发现是transfer的list.js中有个getCheckStatus方法耗时比较多(至于外层为啥那么多层,可以去看看react合成事件的派发)。

image.png 有sourcemap的可以查看antd在最近一次经过babel转换前的源码。我们可以直接在浏览器端断点调试,查看在哪行执行耗时较多。最终发现原因如下图

image.png 卡顿的原因是外层是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的代码

image.png 修改源码打包替换后或者用yarn link后,重启react项目再来看看选中根节点耗时多少

image.png

可以看到目前getCheckBox方法在耗时列表排名中已经不见踪迹了。总耗时由原来十多秒减少到不到一秒。接下来就可以依葫芦画瓢解决transfer其它卡顿问题了。

  • transfer取消选中checkbox卡顿

image.png

  • transfer在targetKeys数据较多时选中/取消选中checkbox卡顿

image.png

  • 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;
};

image.png 至此涉及antd/transfer组件几万数据下卡顿问题就已全部解决。

优化前/后示例

优化前 优化后

结语

追求六便士的路上又多了一颗月亮😊

不足之处期待指正👏👏👏