最近公司项目,客户反馈卡顿的越来越多了,是时候需要治理一波longtask了。项目整体来说就两个页面,一个是列表页一个是详情页,列表页的卡顿是肉眼看见的,先从列表页下手吧。此次目标是将列表页在使用过程中的操作卡顿点解决掉,因为就先不考虑首屏加载了。对于工作台性质的b端后台业务处理系统,更关注的使用中遇到的卡顿问题,对于首屏倒是没那么在意。
卡顿点一: 表格渲染时间太长。
先用默认每页20条数据来看一下大体渲染时间,如下图,在search接口返回后,表格渲染20条件数据大约就得需要800~900ms。如果是每页100条那大约在4s~5s区间。可见此问题确有存在。
问题原因猜测以及定位:
1.先保证在列表接口返回后,表格组件是否只渲染了一次。
2.表格组件里最多的元素是单元格,确认那些简单文本展示的单元格是否只渲染了一次。
3.单元格组件里是否有相对耗时的点,因为单元格太多,只要有一个耗时的点,一循环累计耗时就比较长了。
问题确认以及解决:
1.表格组件render次数确实不止一次,解决办法也很简单,由于是react框架,只需要保证传给表格组件的参数prop不变就可以了,这种问题其实一般出现在prop有参数是引用类型时候,比如函数啊,对象啊之类的。一套useCallback, useMemo、React.memo之类的下来就可以了。
2.单元格渲染中,确实有一段相对较长的代码耗时。这段代码是用来判断单元格内的文本是否需要展示tooltip的,如果文本比较长则截断并展示tooltip。其逻辑如下。
其中getBoundingClientRect这个api,会造成立即回流,虽然回流的元素很少,耗时较短,可是单元格实在太多,以每次1ms来计,20条数据25列,那就是500单元格,耗时都不少。可改成通过字符数跟font-size来大体估算,字符串所需要的宽度,以确定是否展示tooltip,虽未必精准可也可满足需求。代码大体思路如下。
经过这两点处理,重新performance录制了下,每页20条数据longtask耗时由以前的800~900ms优化到300~400ms,每页100条数据longtask耗时由以前的2s左右优化到1s左右。
卡顿点2:表格操作卡顿。
点击下一页时候,search接口发出之前有一段比较明显的卡顿感,理论上来说我点击下页,视图上来看只有两点变化,当前页变为下一页跟表格出现一个loading加载态。按理说这有什么好卡的呢。但是通过performance录制发现确实有有一段longtask。问题得到确认。
问题分析以及确认:
先看页数变化这段。根据火焰图,一点点找,找到一个在公司UI库中耗时较长的函数,此函数是UI库中的分页组件,构造分页筛选项数组时候所用。如下图,而我们列表页的总页数又特别多,线上动辄就是一百多万的分页,而这段代码又使用Array.from + 一个回调函数来构造一个 从5到一百多万的数组,如果是直接用for循环相信是没有这个问题的,大道至简没必要搞太多花里胡哨的操作。
再手动将分页组件改为100页后,此段longtask明显从300ms缩短至100ms左右。剩余这个100ms,只能是表格加载时候Spin组件的loading态了,通过观察dom节点的变化,发现Spin组件loading态跟非loading态时,会更改wrap元素的class,此操作会引起Spin组件下所有节点的回流,所以有一段较长Recalculate Style,其中涉及到需要计算的节点为表格的所有子元素大约2000~3000多个,在注释掉search接口前的loading状态后,果然这段100ms的Recalculate Style的时间没了。
问题解决:
针对分页数较多的情况,经过跟产品商量以及线上打点监控,确认使用1000页以后的人数寥寥无几,因此可把列表分页最大设置为1000页,解决掉那个构造100多万个数字数组的问题。
针对Spin组件在loading态跟非loading态变化造成所有子元素回流的问题,手动简易实现了一个Spin就可以了,不用更改wrap元素的class。
问题三:拖拽组件卡顿。
拖拽的卡顿是个老问题了,尤其在某些dom节点比较多的工作台,表现尤为明显。
问题分析以及确认:
我们用的开源拖拽组件react-rnd,这个组件是对draggble组件跟resize组件的一个组合封装。通过performance录制,当拖拽工具开始跟拖拽工具结束时候,各有一段比较长的longtask,皆为Recalculate style,需要回流的节点数近万,看起来是整个body下的节点都进行回流了。
经过观察拖动开始跟结束时候的dom变化,发现拖动开始时候,body节点新增了一个class:react-draggable-transparent-selection,而结束时候则去掉了这个class。问题得到确认,是更改body的class引起。
下载了react-draggble的源码,然后以react-draggable-transparent-selection 这个class为关键词进行了全文搜索,找到了相对应的代码,此段逻辑由enableUserSelectHack这个prop控制,是用来控制在拖拽过程中,是否可以拖选文档文字的。如果注释掉这段,可能在拖动过程中拖动过快会不小心拖选文字。
问题解决:
考虑到相比于卡顿,这种体验并不影响大局,将此参数enableUserSelectHack设置为false。就此解决拖拽的卡顿问题。
经过上面这几点的优化,列表页的longtask总体数量骤减,卡顿问题基本得到解决,后期问卷中很少再有客户反馈卡顿问题。根据线上longtask打点看板,无论是300ms、400ms、600ms、1000ms以上的整体量级基本优化至最高点的1/6左右,效果比较显著。