vueTable大資料展示優化

Obeing發表於2018-10-09

背景

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也會承受那麼大的壓力,一點點優化都能顯著減輕記憶體。在寫程式碼時要特別關注高頻事件的觸發,一切的優化方向就是在實現功能的前提下減少重新渲染的發生。

相關文章