前端tree優化實踐:渲染速度從14.65s到0.49s

全篇主要思想:遞歸的本質是棧的讀取

先看效果對比

以下都是基於10000條子節點數據作對比,先上最終數據對比:

遞歸版tree,渲染速度: 14.65s,點擊節點處理速度: 9.83s

優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s

遞歸組件實現tree:渲染速度 15.71s -1.06s = 14.65s

遞歸版tree性能圖-1

遞歸組件版tree點擊節點性能分析圖:點擊節點處理速度: 10.19s - 0.357s = 9.833s ≈ 9.83s

遞歸tree點擊節點圖-2

最終優化實現tree: 渲染速度2.25s - 1.76s = 0.49s

優化tree性能圖-3

優化版tree點擊節點性能分析圖:點擊節點處理速度0.623s - 0.3s = 0.3s

優化tree點擊節點圖-4

最終對比是:

遞歸版tree,渲染速度: 12.19s,點擊節點處理速度: 9.52s

優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s

分析問題

我們可以藉助performance分析一下遞歸組件的耗時點所在,上遞歸組件渲染的性能分析:

1.script耗時分析:

遞歸tree script性能分析圖-5

通過圖-1性能瀑布可以清晰的看到script執行佔了8.9s的時間,通過上圖即圖-5可以看到script的的調用棧主要集中在創建vue實例時的createChildren上面。

2.render耗時分析

遞歸tree render性能分析圖-6

通過上圖即圖-6可以清晰的看到render耗時主要集中在Recalculate Style、Layout上面。我們知道Recalculate Style、Layout主要是樣式計算,因此查看代碼:

遞歸組件部分代碼圖-7

發現在遞歸的tree-node組件裏面有很多樣式的計算,10000條子節點就需要計算10000次。

3.DOM結構分析

遞歸組件DOM結構圖-8

實現思想

來看我們的開篇思想:遞歸的本質是棧的讀取。

在算法中我們會遇到很多遞歸實現的案例,所有的遞歸都可以轉換成非遞歸實現,其中轉換的本質是:遞歸是解析器(引擎)來幫我們做了棧的存取,非遞歸是手動創建棧來模擬棧的存取過程。

萬物都是相通的,遞歸組件也可以轉換成扁平數組來實現:

1.更改DOM結構成平級結構,點擊節點以及節點的視覺樣式通過操作總的list數據去實現

2.然後使用虛擬長列表來控制vue子組件實例創建的數量。

優化版實現

主要分爲兩部分功能:

1.tree數據和DOM結構的扁平化;

2.虛擬長列表控制DOM渲染數量。

1.tree數據和DOM結構的扁平化

優化版tree的DOM結構圖-9

由上圖我們可以看到經過改造之後的tree的DOM結構,父節點和子節點是平級的,在操作子節點時去操作內存中的listData數據來改變相關聯節點的狀態。

我們再看下listData數據的結構:

優化版tree listData數據結構圖-10

上圖即圖-10結合圖-9的DOM結構可以對整個功能的實現邏輯一目瞭然:

listData中的每一項的style、checked、path等信息來描述節點的樣式位置和狀態,操作一個節點時通過listData更改相關節點的狀態樣式等信息。

因此最終來寫我們的代碼:

實現.vue代碼圖-11

我們再看下handleCheckChange的做了什麼:

handleCheckChange處理邏輯圖-12

2.虛擬長列表控制DOM渲染數量

實現思路:

根節點DOM分成兩個子節點:fui-tree__phantom 和 fui-tree__content。

兩個子節點都是絕對定位,爲了在滾動時避免數據的更改回頭觸發滾動事件。

虛擬列表DOM組織結構圖-13

根節點解兩個子節點css:

.fui-tree {
    height: 400px;
    overflow: auto;
    position: relative;
  }

  .fui-tree__phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }

  .fui-tree__content {
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
  }

然後我們通過滾動條的位置來計算我們應該要取哪些數據。

主要代碼:

計算可視區數據的起止索引index圖-14

通過startIndex、endIndex可以取出我們需要循環的數據列表renderNodes:

computed: {
      renderNodes() {
        if (!this.treeNode) return []
        return this.treeNode.listData.slice(this.positionConfig.startIndex, this.positionConfig.endIndex)
      },
      phantomStyle() {
        return {
          height: this.allListLen * 20 + 'px'
        }
      }
    }

結合圖-11的v-for,這樣我們在渲染時的dom數量是固定的條數,如下圖:

優化版tree DOM數量是固定的圖-15

虛擬列表的接入可以讓即使再多數據量也能渲染固定的DOM數量,這樣就可以支撐更大數據的渲染和功能。

以上我們實現了業務需求的大數據渲染,目前測試可支撐到20w條節點,點擊子節點時會有肉眼可見的延遲,主要是圖-12中handleCheckChange的數據查找和處理,這塊還有一定的優化空間:使用字典樹存儲節點相關信息,字典樹和扁平數組listData的每一個元素指向同一個內存地址,在handleCheckChange中通過操作字典樹來達到操作listData的元素的效果,經典的空間換時間的案例。

參考鏈接

更多內容,請關注前端之巔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章