24

背景

clipboard.png
大数据项目根据用户输入代码查询数据,用户的代码不可控(比如select from db limit 5000),有可能一页需求要求展示100行5000列数据。由于是用户代码实时查询的数据,后端不可能将所有查询结果都存储。因此,查询的结果是实时的、全量的,分页和排序都需要前端去实现

现状

刚开始的接手项目的时候完全不能展示十万级的数量,chrome标签页直接崩溃。这个在分析了需求,展示数据不需要响应式后,用了Object.freeze()后就可以勉强展示十万级数据。虽然还是卡顿,但是需求已经实现。(值得注意的是Object.freeze()并不是深度冻结,实际应用中对象要进行递归操作。)

下面图展示的是100行乘以一千列,在左右拖拽0-150列。目前也对超过两百列的数据进行横向的懒加载操作,实现原理时监听scroll事件滚动到末尾时截取对应下一组数据,然后将滚动条恢复到头部。可以从gif中明显感觉到这个过程是滚动条恢复到原状之间耗时比较长。而且当用户想要看前一组数据的最后一项和后一组数据的前一项时,js就要不停地做截取数据的操作重新渲染,开销非常大。

clipboard.png

利用chrome devtool performance进行性能分析。(进行性能分析时使用隐身模式避免chrome插件对结果分析造成偏差)

  • 观察FPS图表,有几段红帧证明这过程中页面超负荷,会出现卡顿响应缓慢等。

    clipboard.png

  • 选中红帧区域,Main区域发生变化,变为当前选择时段的函数调用栈详情。点击会在下面的Summary里发现对应的信息以及警告提示回流可能为性能的瓶颈Forced reflow is a likely performance bottleneck.。对应的问题出在监听scroll事件后出现的js代码中,执行的次数非常多。不仅需要去读取scrollLeft值,还因为重新渲染数据时使组件纵向高度发生了改变,进而多次触发了element-ui组件的updateScrollY方法。从Screenshots可以看到这时刚好是数据移动到最后要对数据进行截取重新渲染。
    clipboard.png
  • 从上图summary可以看到js的运行压力很大,勾选memory项纪录js heap占用情况,查看到占用高达161mb-325mb
    clipboard.png

优化要点

  • table的数据并不会有修改的需要,仅仅是展示,并不需要响应式。Object.freeze()可以阻止vue追踪属性的变化,减少性能的开销
  • 由于数据展示的table不仅大量而且经常变换数据集。为了减少回流和重绘,table做绝对定位脱离文档流,避免布局抖动。
  • 由于后端返回的数据一组表头和内容分开的数组,而开源element-ui的vue组件都是以key:value的形式,大量数据情况下仅仅是将数组转化为key:value的形式就花费掉几百毫秒的时间。开源组件能解决的是通用情况,这种情况下为了尽量减少开销重写适用于业务的table组件还是很有必要的。

    //后端返回的格式
    data = [
        columnName: ['col1', 'col2', ……],
        columns: [
            ['1', '2', ……],
            ['1', '2', ……],
            ……
        ]
    ]
    // 开源组件需要的格式
    data = [
        {
            col1: '1',
            col2: '2',
            ……
        },
        {
            col1: '1',
            col2: '2',
            ……
        }
    ]
  • 后端返回的数据量有可能高达百万级,尽管前端进行分页还是有可能要展示到数量达十万。其中行最多每页只展示100条,但是列由ide用户执行的代码决定,这里主要影响性能的是列数。列数有可能为1000条,模拟横向懒加载,将拿回来的数组截取部分展示,减少页面上的dom节点。但是目前模拟懒加载的方式用户体验不好。

    • 为了解决横向滚动时相邻列的数据能够展示在同一屏上,而不需多次来回切换,首先做的工作是在截取数据时保留前一屏的数据,拖动后滚动条回到中间位置,在一定范围内不需要多次滚动才能查看。(如下图)

      clipboard.png

- 但这种方式也是非常不友好,每次滚动到最后要去检测用户是否按着鼠标有没有抬起,防止触发多次数据重新渲染。因为这种情况下,用户拖一次只能加载一组新数据,滚动条便回到了中间位置,如果用户需要看到最后一组数据就要多次操作。正常的懒加载应该是有一条适应高度的滚动条拖拽,无缝连接。

- 懒加载方式常见的有:
    1. 淘宝一屏用元素占据一定的高度,然后再去拉图片数据。滚动条便适应高度的拖动距离。但这种方式还是需要元素占位,淘宝一页的数据量其实不算大,因为它结合了分页。
    2. 掘金沸点的无限加载:掘金的方式是监听到底部时,再去拉响应的数据追加,滚动条会自适应滚到相对应的地方。但是掘金这种懒加载一直加载数据没有截取掉旧数据,所以滚动条距离也是一直适应数据的。尝试将掘金沸点一直拖动到2000条,网页已经开始有点卡顿。而在ide项目中,两千条数据算是少量数据。
- 启发于[https://github.com/tangbc/vue-virtual-scroll-list](vue-virtual-scroll-list),利用了padding值模拟了淘宝固定高度,不需要元素占位,模拟出全部数据量的滚动条纵向滚动距离,拖动时完全无感知数据的重新渲染。目前vue-virtual-scroll-list只支持纵向,但稍微改造下就能用在ide项目的横向懒加载。(改造后如下图,gif软件录制时稍微有点卡顿感)

clipboard.png

  • scroll长时间运行的重新计算样式事件,其时间如果超过 16.7 毫秒,并且恰好发生在滚动期间,导致用户体验到明显的抖动。为了在拖动过程中数据变化以连贯、平滑进行过渡,函数节流改setTimeout为requestAnimationFrame(rAF),由系统来决定回调函数的执行时机;它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致渲染数据出现卡顿的问题,并且rAF能兼容到ie9以上了。

优化后结果分析

拖动几百条数据截取的performance在FPS图表中已经没有最初的红标,没有Forced reflow,每帧的rendering也由rAF控制在16.7ms以内,js内存占用也从161mb-325mb,降低到157mb-196mb。

clipboard.png

组件接口设计原则

复用性:配置参数的方式去差异化体现,参数的可配置性提高了组件的复用率和灵活性。
可维护性:组件化后,组件内部的逻辑只对组件负责,外部的逻辑只通过配置参数适配,提高了代码的逻辑清晰度,可以快速定位代码出现问题的地方。

这个组件设计时对外提供toLefttoRightonScroll事件,分别是滑动过程中到了头、尾,及滑动过程的回调。提供了offsetremainbench参数表示刚渲染时的偏差,显示的列数,及保留多少列在实际dom中。

小结

以前没有想过js也会承受那么大的压力,一点点优化都能显著减轻内存。在写代码时要特别关注高频事件的触发,一切的优化方向就是在实现功能的前提下减少重新渲染的发生。


Obeing
665 声望108 粉丝

努力地成为一只小牛