坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.5.8

前言

性能问题算是少见的了,尤其是还需要借助浏览器performance的。

这么有趣的事情,当然要记录下来~


问题

内容背景

我公司是搞SaaS(Software as a Service)的,提供低代码平台给用户自定义小程序等内容编辑。

既然是支持自定义编辑,那么自由度就会比较高,所以低代码平台一般左中右这种布局,中间是内容区,左边放可以拖拽到中间内容区的模块,右边是这个模块可以自定义的项。

这些模块中包含最基础的:文本、按钮、图片、自由容器(前面三种模块的容器,它们可以在自由容器中拖拽)。

由于公司成本控制要求,我在的部门需要接入中台的模块,也包括上面提到的最基础的模块。

这个平台我一般称为:设计器,注意是在PC web端的。用户的小程序是C端

那么背景就说到这。

技术背景

项目采用的是vue2全家桶,很普通的一个项目

bug

在接入中台的基础模块后,部分测试账号发现在自由容器中拖拽这些基础模块的时候拖拽的模块位置出现偏移,没有和鼠标位置对齐:

那这就很奇怪了,因为开发的过程中并没有遇到。

很有可能是拖拽那一刻位置计算有误,但是量少的时候没有这个问题。。

接下来进入分析的流程


分析

复现

要处理bug,第一步自然是要复现这个bug,不然只是猜想空谈。

在对比了不同的测试账号之后,发现页面中模块多的测试账号很容易复现,那么针对这点,在本机上添加超过40个模块之后发现可以不稳定复现,模块越多越容易复现。

越多越容易复现?!

那么这可能是一个性能问题!!我的天,你都不敢想象我当时有多兴奋,毕竟在体量不是很大的项目上可以说是万年难得一见。

总结问题

问题应该出现在拖拽那一刻,导致计算没跟上鼠标的位置。

排查bug在哪个项目上

因为涉及两个项目,所以这里需要判断异常代码是在哪个项目上。

在排查了部门项目代码之后,发现并没有重的逻辑会引起性能问题,而且看表现,应该是偏底层的,很有可能是中台的逻辑异常。

那要怎么确认呢?

中台有提供基础的平台,如果可以在上面复现,那么就可以确定下来是中台那边的问题。

同体量的模块在中台那边并不能复现,于是我开始怀疑人生了。。。

思考了好一会儿之后,选择加大体量,我就是不信邪~

果然,在体量为之前的两倍左右之后开始不稳定复现,越多越容易复现。

那么问题来了,怎么知道是哪块代码的问题?

这里有两种方案:

  1. 通过测试用例,一步一步抽丝剥茧
  2. 基于浏览器的dev Tools

这里我选择第二种方案,基于以下两点:

  1. 被流放到处理缺陷之后,我学会了在source中打断点调试,并且已经轻车熟路,本机项目也万年没怎么开过了
  2. 性能问题,我们可以借助于浏览器开发者工具的performance面板去分析啊!

performance捕捉行为

performance可以允许我们去捕捉某一段的行为,然后分析这个过程中的性能问题

欸,这里不就有可能出现性能问题?

确认位置

确认代码位置我就不多说了,这涉及到公司的代码~~

只能说是来自于中台依赖的一个底层库,它实现了项目中所有的拖拽逻辑。(另外performance捕捉到的都是这个库里的,所以想当然的去找这个库)

并且根据我们上面总结的问题

一次错误的分析

拿到这个截图后我很开心啊,因为重排/重绘确实会因为dom变多布局变复杂而导致性能问题

跟着截图中的函数名,我找到了具体的位置:

(这段代码不涉及什么敏感的信息,所以直接放出来了)

这一段确实有添加样式和添加dom的行为,但是往head里添加元素并不会造成重排行为,另外这里的样式只是调整cursor,虽然用了通配符*,但是也并不会引起重绘和重排:

补充一下八股文:

  1. 重排(reflow也叫回流,页面初始化时叫做布局)一般是在发生在页面布局发生变化的时候,比如某个有明确占空间的元素大小形状发生了变化(注意,通过transform或者没有展示的都不会引起),另外还有一些骚操作比如获取元素的offsetHeightoffsetWidth,这会让浏览器强制同步这元素的布局,可能会引起重排。
  2. 重绘:(repaint,页面初始化时叫做绘制),一般发生在元素颜色等不会影响到布局的样式发生变化。

不过我还是去验证了,因为第一次遇到这种问题,还是按流程过一遍。

在去除相关代码后依旧存在问题,那就说明不是这块问题。

其它还有一些,比如拖拽之前会给一大块区域添加以下样式:

然而这俩也并不会引起重排和重绘

再次分析

如果不是重绘/重排的话,那会是什么呢?

模块多,那就代表dom多,dom变多会引起性能问题的除了重排/重绘之外,也有可能是用了一些需要遍历全局domapi,比如Document: querySelectorAll

欸!你别说,还真有用到:

这个函数是用来获取到要拖拽元素的父元素的,在mousedown事件中,来看下这块的耗时:

耗时算是很高了。

补充

这里我需要简单的说下这里的原理:

这里涉及到两个项目,一个是这个拖拽的,另一个是中台项目。

中台项目中对目标包裹一个wrapper元素,这个元素绑定了@mousedown@mousemove两个事件。

而拖拽的库中则是监听的documentmousedownmousemove

拖拽的元素实际上是mousedown时复制的一个ghost元素,然后动态改变这个元素位置(库中)

所以可能存在触发了元素的@mousemove之后,document监听的逻辑还在mousedown中。

也就是创建这个ghost元素时的位置已经不是原来的位置了

验证

不过要怎么验证呢?

很简单,我们在关键点打上log(这种就不适合打断点了,因为move事件是连续触发的)。

querySelectorAll前后加上log(在拖拽库中documentmousedown事件回调中),然后在wrapper那边@mousemove加上log

如果真是querySelectorAll耗时的问题,那么在第二个querySelectorAlllog前应该已经触发了wrappermove事件。

确实如此!

这里你可能会有疑问,为什么是创建ghost元素的起始位置问题?后面直接让它跟踪鼠标的位置不就好了?

因为这个库创建的元素之后并不是用的绝对值赋值给这个元素来改变它拖拽的位置,而是相对值来计算的,所以一开始错,后面就都是错的。


临时解决方案

知道了问题,那么就是解决方案了。这里querySelectorAll不太好去掉,所以我们选择简单处理:绕过去,既然第二个log前就触发了move事件,那么我们把位置信息的获取放到前面不就好了。。

实际上人中台大佬也是这么做的


总结

这个bug被修复实际上快一年了,最近整理的时候才发现我最开始的分析有问题。。。

性能问题是可遇不可求的(当然,主动去分析还是有很多的),所以要好好珍惜。