记一次TableSchedule.js性能分析优化

其实一直知道它有性能问题,就是一次往里面添加很多课/活动的时候,会卡那么一两秒(本项目一般情况,当然数量越多越长,考虑要不要加一个限制…),不过没太在意。

这两天闲了点,一个主要bug也解决了,就试着优化一下性能。

打开chrome的performance面板,录制一次生成课表(初始化并一次往里添加150+活动项)的过程,如下图:

一次生成耗时2s+

看到下面一个个紫色带红色小三角的小邪恶了吗,放大看一下:

单个addEvent调用有两个这样的小邪恶,总耗时10ms+,乘以150,占总耗时大部分。同时,下面有chrome很贴心的友情链接(需要科学上网),里面提到反复读取offsetWidth 并用其更新元素样式是“更糟”的问题。

对号入座,我这里确实在_posToMinute中读取了已添加到DOM的元素的offsetTop/Height,目的是在添加新项目时和已有项目的时间比较,看已有的列中有没有空位置可以放得下。后续的添加元素到DOM应该只会比更新元素样式更耗时吧……

我们来验证一下。找到_posToMinute这个函数,在读取offsetTop/Height的代码前后设置两个logpoint,前一个是console.time(),后一个是console.timeEnd(),如图:

设置logpoint验证读取offsetTop/Height耗时

重新生成一次,得到结果如下:

比较长的一次耗时达30ms+

找到问题所在,开始修改代码,修改如下:

我自己都笑了,之前的逻辑已经在元素上添加了startmendm这两个dataset项,结果搜了下整个仓库的代码完全没用上,大概当时只是顺便添上以防以后可能会需要吧,现在确实需要了。


除了上面_posToMinute中的读取offsetTop/Height导致的性能问题,当组件的labelGroups设置为true时也会造成性能问题。

打开这个选项,生成一次,结果如下:

_setGroupHeader耗时

_setGroupHeader中,也是两个小邪恶。在这个github gist这篇博文 中都提到getBoundingClientRect也会导致浏览器强制同步布局。

这个方法的确在getRect这个工具函数中有用到,找到这个函数,设置logpoint来验证一下:

设置logpoint

结果如下,看来这个才是大头啊:

getRect耗时

调用这个函数的目的是在表头添加分组标签后,更新_coords.grid,这组坐标作为一个参考,用于判断拖拽时是否抵达表格可视部分的边界,以及计算活动项在表格中的相对位置。

手动添加一个活动项时,每添加一个都更新一次没什么问题,因为可能之前没有带分组的活动项,新添加一个带分组的,那么添进去以后表头就会多出来一行。但现在是从api获取一堆活动项的数据后一并添加进去,如果能做到全部添加进去之后再更新就好了。

但那样应该要改不少代码,可能还需要一个新的专门批量添加的API,比如addEvent<strong>s</strong>。并不想往复杂了搞,于是想了下面的办法:

    let timeoutRef = null // 循环外部
            // ...
            // 循环内部
            clearTimeout(timeoutRef)
            timeoutRef = setTimeout(() => {
                clearTimeout(timeoutRef)
                timeoutRef = null
                this._coords.grid = getRect(this.el.dayGrids[0], this.el.scroll)
            }, 0)

放在setTimeout里,timeout为0,因为执行栈中一直有任务(_setGroupHeader一直在循环调用),所以回调函数会被放到任务队列里。每次循环先把之前设置的计时器清除,这样到最后循环执行完毕,回调函数就只会被取出执行一次。完美。

最终优化结果如下,800ms,还是有点卡,但比之前好多了。可以看到最右边的一小部分就是计时器回调函数被取出执行。

最终结果

到此告一段落。严格说不算优化,应该说:补足缺失的部分、抛弃错误的做法。

应该还有再优化的空间,就是后话了。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据