Vue - Table表格渲染上千數據優化
這次項目經驗會談談經常在項目中,針對成千上萬數據渲染優化的不斷探索來談談自己的體會,其目的就是保證用戶瀏覽上萬條數據的時候,UI要很流暢,確保用戶操作過程中不會出現UI卡頓或者最糟糕的情況,直接瀏覽器奔潰。
其優化目錄如下,由於內容很多,會分兩篇文章進行研究,
本文章主要會圍繞如何設計一個虛擬滾動來渲染成千上萬的數據。
1.表格佈局( To be continue )
2.Reflow & Repaint渲染方式( TBC )
3.瀏覽器API:window.requestAnimationFrame渲染優化( TBC )
4.虛擬滾動:Virtual scrolling
GITHUB-Vue Table表格渲染上千數據:後續會加入Filter功能,針對Reflow和RequestAnimationFrame的渲染效果會更加明顯

虛擬滾動(Virtual scrolling)篇
關鍵字:虛擬行渲染,transform數據移動,列表節點渲染優化
NK0 - Background
本文的前提條件是前端已經緩存了上萬條數據,當渲染數據到UI上時,如何讓用戶在使用過程中不會遭到卡頓,用起來不流暢?
之前很多項目由於數據大體圍繞在上千左右,當結果集結合其它功能,例如Filter數據過濾功能一起使用的話,會採用以下幾種方式去優化列表渲染,
(1)Version1: 採用display:none(對應Vue的指令是v-show),再結合v-for中的Key來復用DOM元素和隱藏元素=》該方式會導致UI Reflow即回流,所以UI會經常出現短暫的卡頓,用戶體驗不是很好
(2)Version2: 採用window對象的內置API,即window.requestAnimation(),來渲染成千上萬條數據,即模擬動畫效果讓用戶操作起來特別的順暢。
以上兩種方式都有個特點:DOM中會插入數據量大小的元素,儘管有些數據被隱藏起來,但都會導致HTML文件size會非常大,低端設備的速度會明顯變慢。
所以基於以上兩種方式,還不能夠滿足去渲染上萬條數據!
也因此trigger出本文的主題- 基於VUE列表的虛擬滾動
本文的虛擬滾動方式主要圍繞著
(1)虛擬行渲染:緩存數據和篩選數據,除了要保留用戶的可視區域的數據,還考慮到瞭如果用戶的滾動範圍不是很大的話,就不需要去刷新頁面,所以DOM中的元素除了可視區域的數據,會多保留視圖的上下留閒數據。
(2)佈局:主要是為了假裝所有數據元素都有在佔用空間,瀏覽過React virtualized庫,發現它在復用已有DOM元素的基礎上,通過css的絕對定位position:absolute + top:偏移量,來移動數據,但是這樣滾動元素的時候會引起瀏覽器回流,會增加更多的渲染開銷,所以我這邊會採用另一種方式,即transform:translateY(偏移量)來優化數據移動,因為該CSS3元素不會引起Reflow和Repaint。
(3)DOM復用:使DOM節點的數量保持在較低的水平,因為DOM節點如果太大而無法管理,低端設備的速度會明顯變慢,所以我們能做的是複用已有的DOM節點和減少每個節點的佈局、樣式和繪製方面的開銷成本。而VUE提供了數組全新賦值和變異方法來復用DOM和減少DOM操作,但是數組全新賦值的開銷成本比數組變異開銷更大,所以針對用戶的滾動速度來優化列表渲染。
NK1 -虛擬滾動(Virtual Scrolling)
TL;DR :復用DOM元素,隱藏不在視圖內的其它元素,使用佔位符來延遲刷新數據。
下面是當我們用虛擬滾動方式來處理上萬條數據在UI的呈現效果,

從上面的結果發現,當用鼠標滾動的時候,
(1)儘管數據量是上萬條,但是HTML標籤元素永遠就那麼幾個
(2)有些HTML元素會被更新,而有些HTML元素不會變化
虛擬滾動能夠很好優化上萬條數據的呈現跟的上用戶滾動操作的速度,猶如動畫般的交互順滑。
接下來會分享下虛擬滾動的設計實現,
NK2 Virtual scrolling - Props Design(屬性設計)

(1)viewport:這裡看成是Table數據的可視區域,需要提供可視區域的高度,用於計算實際渲染在DOM中Item的數量。
(2)size:每條數據在DOM中佔用的高度,用於統計虛擬列表的高度,默認每行的高度為40px。
(3)Render Items:真正曾現在用戶視覺上的items。
(4)Remain Items:(向上/向下)可是區域之外的留閒數據高度,不顯示在viewport中,但是存在DOM中,其用於當用戶滾動的距離不是很大的時候,UI不需要重新去渲染,為滾動做了一層留長優化。
接下來講一下虛擬滾動特徵,
NK3 Virtual scrolling - Virtual Row Render(虛擬行渲染)
Virtual Row Render -在已知viewport有高度情況下,我們可以先把每條數據看作是每一個獨立的行數據,用索引來標記每條數據,用Map對象封裝這些塊數據,存在瀏覽器的內存中,當滾動事件被觸發的時候,我們只需要渲染能夠映射在viewport中對應的塊數據,而不是遍歷渲染所有塊,能有效的減少HTML file size。

key = index,我們用數據索引和每條數據的高度來作為虛擬塊Map的Key,結合滾動的距離和viewport的高度,來標記實際要渲染在DOM中的items,即

另外每個Item會根據Key值來定義其顯示範圍,如item1的key為0,則它的顯示範圍應該是[0~40)這個區間,而item2區間為[40,80),以此類推… …其目的是用於計算item的顯示區間是否在滾動的範圍內。
NK4 Virtual scrolling -數據移動設計
場景:當我們的列表有上萬條數據,我們給定
每條數據的高度為40px,即itemHeight = 40px,
留閒高度為80,即remainHeight = 80px,
而表格的可視區域高度為80px,即viewportHeight = 80px,
由此我們可以設計存在DOM中的items最多可以渲染6條。

所以當我們在滾動的時候,當滾動的距離到item12的高度的時候,此時我們希望可視區域的數據會被刷新,並且DOM元素會被跟新如下,
但是,當用戶滾動的距離不是很大的話,例如它從item1滾動到item4的時候,我們希望DOM不需要被跟新,即
接著我們來細節化模擬數據滾動,如下圖
實線,例如item1 ~ item6,代表已經在DOM中存在的數據
黑色實體,例如item1~item2,顯示在可視區域的數據,
反之灰色實體item3~item6的被隱藏了起來
而虛擬列表的數據Size決定了可視區域滾動條的大小
滾動公式設計,主要是如何確定Dom items的高度範圍,即

向下滾動場景模擬: 當我們滾動的距離小於向上的remainHeight(80px)的時候,
其minDomItemHeight = 0,
maxDomItemHeight = viewportHeight + 2 * remainHeight(向上/向下留閒) = 240px;
即

但DOM中的Items為[item1,item2,item3,item4,item5,item6] -> 沒有變化
當我們向下滾動到100px的時候,
DOM中的Items為[item1,item2,item3,item4,item5,item6, item7 ] ->新增了一個item7

當我們滾動到120px的時候,
DOM中的Items為[item2,item3,item4,item5,item6,item7] -> item1DOM元素被移除了。

……
所以按照用戶滾動的趨勢,我們統計了滾動距離時,我們的數據渲染情況如下

即滾動公式
向下取整minItemHeight = scrollTop > remainHeight ? Math.floor((scrollTop - remainHeight)/ itemHeight) * itemHeight : 0;
向上取整maxItemHeight = scrollTop > remainHeight ? (Math.ceil((scrollTop + viewPortHeight + remainHeight) / itemHeight) )* itemHeight : defaultRenderItemsHeight;
而defaultRenderItemsHeight需要跟viewport的高度和留長高度,來決定渲染在DOM中item的數量.
const renderItems = Math.ceil(viewPortHeight / itemHeight) + 2 * remainItems; const renderItemsHeight = renderItems * itemHeight;
NK5 Virtual scrolling - CSS3 transform優化數據移動
當數據往上/下滾動的時候,我們需要復用和移動DOM中的元素,使其能根據我們算出的高度顯示在viewport中,一般情況會使用position:relative + top來進行元素垂直方向的偏移,但是我們知道當採用top屬性,它會使UI Reflow,性能不是很好,所以我們採用CSS3中的transform變形屬性來移動,其優點是不會是UI重新的Reflow和Repaint.
transform - translateY的特點如下,
(1)範圍:適用所有的HTML標籤元素
(2)它是指Y軸(垂直軸)方向的移動,單位可以是px,em或百分比等
(3)當y為正時,表示元素在垂直方向向下移動;
當y為負時,表示元素在垂直方向向上移動,跟我們的數學坐標係不同
而
(4)性能優化:元素移動,不會引起Reflow和Repaint
所以我們在初始化/跟新要渲染數據的時候,可以為其綁定translateY的值
(translateY=itemIndex * itemHeight),如,

NK6 Virtual scrolling -列表渲染優化
我們來比較下數組跟新的兩種方式:變異方法和替換數組如下

即變異方式和替換數組方式中的索引值替換,會復用需要更改的DOM元素,
而數組全新賦值方式則會復用渲染整個列表(DOM元素移位),
當滾動元素的時候,數組全新賦值節點渲染情況如下,
而局部跟新節點的渲染取下,
所以當我們用translateY屬性來進行元素位置移動的時候,
即使元素插入DOM的位置不是按順序排列的,但是translateY能確保其元素它在垂直方向的距離如下圖,
紅色區域代表當鼠標往下滾動的時候,需要跟新的DOM元素
黃色區域則代表不需要重新渲染

所以針對列表的優化渲染,建議不要對數組全新賦值,可以考慮用數組替換+數組變異的方式來復用已有的節點。如果前後數組變化完全不相等的話,可以直接使用數組全新賦值方式。
彩蛋:使用AVA成為該項目的測試驅動框架,整個過程採用的是TDD(測試驅動開發)來實踐數據移動的功能模塊,例如,
當要計算一個minDomItemHeight的時候,其測試用例如下,

而功能實現塊:

當要計算一個maxDomItemHeight的時候,其測試用例如下,

當要計算一個minDomItemHeight和maxDomItemHeight的時候,其測試用例只需要一個來驗證就行,其結果如下,

而功能實現
