Vue - Table表格渲染上千數(shù)據(jù)優(yōu)化
Vue - Table表格渲染上千數(shù)據(jù)優(yōu)化
8 個(gè)月前 · 來自專欄 前端項(xiàng)目經(jīng)驗(yàn)總結(jié)
這次項(xiàng)目經(jīng)驗(yàn)會(huì)談?wù)劷?jīng)常在項(xiàng)目中哗蜈,針對(duì)成千上萬數(shù)據(jù)渲染優(yōu)化的不斷探索來談?wù)勛约旱捏w會(huì),其目的就是保證用戶瀏覽上萬條數(shù)據(jù)的時(shí)候访得,UI要很流暢,確保用戶操作過程中不會(huì)出現(xiàn)UI卡頓或者最糟糕的情況衷佃,直接瀏覽器奔潰。
其優(yōu)化目錄如下,由于內(nèi)容很多,會(huì)分兩篇文章進(jìn)行研究淮韭,
本文章主要會(huì)圍繞如何設(shè)計(jì)一個(gè)虛擬滾動(dòng)來渲染成千上萬的數(shù)據(jù)垢粮。
1.表格布局(To be continue)
2.Reflow & Repaint渲染方式(TBC)
3.瀏覽器API:window.requestAnimationFrame 渲染優(yōu)化(TBC)
4.虛擬滾動(dòng):Virtual scrolling
GITHUB-Vue Table表格渲染上千數(shù)據(jù) : 后續(xù)會(huì)加入Filter功能,針對(duì)Reflow和RequestAnimationFrame的渲染效果會(huì)更加明顯
虛擬滾動(dòng)(Virtual scrolling)篇
關(guān)鍵字:虛擬行渲染靠粪,transform數(shù)據(jù)移動(dòng)蜡吧,列表節(jié)點(diǎn)渲染優(yōu)化
NK0 - Background
本文的前提條件是前端已經(jīng)緩存了上萬條數(shù)據(jù),當(dāng)渲染數(shù)據(jù)到UI上時(shí)占键,如何讓用戶在使用過程中不會(huì)遭到卡頓昔善,用起來不流暢?
之前很多項(xiàng)目由于數(shù)據(jù)大體圍繞在上千左右畔乙,當(dāng)結(jié)果集結(jié)合其它功能君仆,例如Filter數(shù)據(jù)過濾功能一起使用的話,會(huì)采用以下幾種方式去優(yōu)化列表渲染,
(1)Version1: 采用display:none(對(duì)應(yīng)Vue的指令是v-show)返咱,再結(jié)合v-for中的Key來復(fù)用DOM元素和隱藏元素 =》該方式會(huì)導(dǎo)致UI Reflow即回流钥庇,所以UI會(huì)經(jīng)常出現(xiàn)短暫的卡頓,用戶體驗(yàn)不是很好
(2)Version2: 采用window對(duì)象的內(nèi)置API咖摹,即window.requestAnimation()评姨,來渲染成千上萬條數(shù)據(jù),即模擬動(dòng)畫效果讓用戶操作起來特別的順暢萤晴。
以上兩種方式都有個(gè)特點(diǎn):DOM中會(huì)插入數(shù)據(jù)量大小的元素吐句,盡管有些數(shù)據(jù)被隱藏起來,但都會(huì)導(dǎo)致HTML文件size會(huì)非常大店读,低端設(shè)備的速度會(huì)明顯變慢嗦枢。
所以基于以上兩種方式,還不能夠滿足去渲染上萬條數(shù)據(jù)两入!
也因此trigger出本文的主題 - 基于VUE列表的虛擬滾動(dòng)
本文的虛擬滾動(dòng)方式主要圍繞著
(1)虛擬行渲染:緩存數(shù)據(jù)和篩選數(shù)據(jù)净宵,除了要保留用戶的可視區(qū)域的數(shù)據(jù),還考慮到了如果用戶的滾動(dòng)范圍不是很大的話裹纳,就不需要去刷新頁面择葡,所以DOM中的元素除了可視區(qū)域的數(shù)據(jù),會(huì)多保留視圖的上下留閑數(shù)據(jù)剃氧。
(2)布局:主要是為了假裝所有數(shù)據(jù)元素都有在占用空間敏储,瀏覽過React virtualized庫,發(fā)現(xiàn)它在復(fù)用已有DOM元素的基礎(chǔ)上朋鞍,通過css的絕對(duì)定位position:absolute + top:偏移量已添,來移動(dòng)數(shù)據(jù),但是這樣滾動(dòng)元素的時(shí)候會(huì)引起瀏覽器回流滥酥,會(huì)增加更多的渲染開銷更舞,所以我這邊會(huì)采用另一種方式,即transform:translateY(偏移量)來優(yōu)化數(shù)據(jù)移動(dòng)坎吻,因?yàn)樵揅SS3元素不會(huì)引起Reflow和Repaint缆蝉。
(3)DOM復(fù)用:使DOM節(jié)點(diǎn)的數(shù)量保持在較低的水平,因?yàn)镈OM節(jié)點(diǎn)如果太大而無法管理瘦真,低端設(shè)備的速度會(huì)明顯變慢刊头,所以我們能做的是復(fù)用已有的DOM節(jié)點(diǎn)和減少每個(gè)節(jié)點(diǎn)的布局、樣式和繪制方面的開銷成本诸尽。而VUE提供了數(shù)組全新賦值和變異方法來復(fù)用DOM和減少DOM操作原杂,但是數(shù)組全新賦值的開銷成本比數(shù)組變異開銷更大,所以針對(duì)用戶的滾動(dòng)速度來優(yōu)化列表渲染您机。
NK1 - 虛擬滾動(dòng)(Virtual Scrolling)
TL;DR: 復(fù)用DOM元素穿肄,隱藏不在視圖內(nèi)的其它元素年局,使用占位符來延遲刷新數(shù)據(jù)。
下面是當(dāng)我們用虛擬滾動(dòng)方式來處理上萬條數(shù)據(jù)在UI的呈現(xiàn)效果被碗,
從上面的結(jié)果發(fā)現(xiàn)某宪,當(dāng)用鼠標(biāo)滾動(dòng)的時(shí)候,
(1)盡管數(shù)據(jù)量是上萬條锐朴,但是HTML標(biāo)簽元素永遠(yuǎn)就那么幾個(gè)
(2)有些HTML元素會(huì)被更新兴喂,而有些HTML元素不會(huì)變化
虛擬滾動(dòng)能夠很好優(yōu)化上萬條數(shù)據(jù)的呈現(xiàn)跟的上用戶滾動(dòng)操作的速度,猶如動(dòng)畫般的交互順滑焚志。
接下來會(huì)分享下虛擬滾動(dòng)的設(shè)計(jì)實(shí)現(xiàn)衣迷,
NK2 Virtual scrolling - Props Design(屬性設(shè)計(jì))
(1)viewport:這里看成是Table數(shù)據(jù)的可視區(qū)域,需要提供可視區(qū)域的高度酱酬,用于計(jì)算實(shí)際渲染在DOM中Item的數(shù)量壶谒。
(2)size:每條數(shù)據(jù)在DOM中占用的高度,用于統(tǒng)計(jì)虛擬列表的高度膳沽,默認(rèn)每行的高度為40px汗菜。
(3)Render Items:真正曾現(xiàn)在用戶視覺上的items。
(4)Remain Items:(向上/向下)可是區(qū)域之外的留閑數(shù)據(jù)高度挑社,不顯示在viewport中陨界,但是存在DOM中,其用于當(dāng)用戶滾動(dòng)的距離不是很大的時(shí)候痛阻,UI不需要重新去渲染菌瘪,為滾動(dòng)做了一層留長(zhǎng)優(yōu)化。
接下來講一下虛擬滾動(dòng)特征阱当,
NK3 Virtual scrolling - Virtual Row Render(虛擬行渲染)
Virtual Row Render - 在已知viewport有高度情況下俏扩,我們可以先把每條數(shù)據(jù)看作是每一個(gè)獨(dú)立的行數(shù)據(jù),用索引來標(biāo)記每條數(shù)據(jù)弊添,用Map對(duì)象封裝這些塊數(shù)據(jù)录淡,存在瀏覽器的內(nèi)存中,當(dāng)滾動(dòng)事件被觸發(fā)的時(shí)候油坝,我們只需要渲染能夠映射在viewport中對(duì)應(yīng)的塊數(shù)據(jù)嫉戚,而不是遍歷渲染所有塊,能有效的減少HTML file size免钻。
key = index彼水,我們用數(shù)據(jù)索引和每條數(shù)據(jù)的高度來作為虛擬塊Map的Key崔拥,結(jié)合滾動(dòng)的距離和viewport的高度极舔,來標(biāo)記實(shí)際要渲染在DOM中的items,即
另外每個(gè)Item會(huì)根據(jù)Key值來定義其顯示范圍链瓦,如item1的key為0拆魏,則它的顯示范圍應(yīng)該是[0~40)這個(gè)區(qū)間盯桦,而item2區(qū)間為[40,80),以此類推……其目的是用于計(jì)算item的顯示區(qū)間是否在滾動(dòng)的范圍內(nèi)渤刃。
NK4 Virtual scrolling - 數(shù)據(jù)移動(dòng)設(shè)計(jì)
場(chǎng)景:當(dāng)我們的列表有上萬條數(shù)據(jù)拥峦,我們給定
每條數(shù)據(jù)的高度為40px,即itemHeight = 40px,
留閑高度為80卖子,即remainHeight = 80px,
而表格的可視區(qū)域高度為80px略号,即viewportHeight = 80px,
由此我們可以設(shè)計(jì)存在DOM中的items最多可以渲染6條。
所以當(dāng)我們?cè)跐L動(dòng)的時(shí)候洋闽,當(dāng)滾動(dòng)的距離到item12的高度的時(shí)候玄柠,此時(shí)我們希望可視區(qū)域的數(shù)據(jù)會(huì)被刷新,并且DOM元素會(huì)被跟新如下诫舅,
但是羽利,當(dāng)用戶滾動(dòng)的距離不是很大的話,例如它從item1滾動(dòng)到item4的時(shí)候刊懈,我們希望DOM不需要被跟新这弧,即
接著我們來細(xì)節(jié)化模擬數(shù)據(jù)滾動(dòng),如下圖
實(shí)線虚汛,例如item1 ~ item6匾浪,代表已經(jīng)在DOM中存在的數(shù)據(jù)
黑色實(shí)體,例如item1~item2泽疆,顯示在可視區(qū)域的數(shù)據(jù)户矢,
反之灰色實(shí)體item3~item6的被隱藏了起來
而虛擬列表的數(shù)據(jù)Size決定了可視區(qū)域滾動(dòng)條的大小
滾動(dòng)公式設(shè)計(jì),主要是如何確定Dom items的高度范圍殉疼,即
向下滾動(dòng)場(chǎng)景模擬: 當(dāng)我們滾動(dòng)的距離 小于 向上的remainHeight(80px)的時(shí)候梯浪,
其minDomItemHeight = 0,
maxDomItemHeight = viewportHeight + 2 * remainHeight(向上/向下留閑) = 240px;
即
但DOM中的Items為 [item1,item2,item3,item4,item5,item6] -> 沒有變化
當(dāng)我們向下滾動(dòng)到100px的時(shí)候,
DOM中的Items為 [item1,item2,item3,item4,item5,item6,item7] -> 新增了一個(gè)item7
當(dāng)我們滾動(dòng)到120px的時(shí)候瓢娜,
DOM中的Items為 [item2,item3,item4,item5,item6,item7] -> item1DOM元素被移除了挂洛。
……
所以按照用戶滾動(dòng)的趨勢(shì),我們統(tǒng)計(jì)了滾動(dòng)距離時(shí)眠砾,我們的數(shù)據(jù)渲染情況如下
即滾動(dòng)公式
向下取整 minItemHeight = scrollTop > remainHeight ? Math.floor((scrollTop - remainHeight)/ itemHeight) * itemHeight : 0;
向上取整 maxItemHeight = scrollTop > remainHeight ? (Math.ceil((scrollTop + viewPortHeight + remainHeight) / itemHeight) )* itemHeight : defaultRenderItemsHeight;
而defaultRenderItemsHeight需要跟viewport的高度和留長(zhǎng)高度虏劲,來決定渲染在DOM中item的數(shù)量.
const renderItems = Math.ceil(viewPortHeight / itemHeight) + 2 * remainItems;
const renderItemsHeight = renderItems * itemHeight;
NK5 Virtual scrolling - CSS3 transform優(yōu)化數(shù)據(jù)移動(dòng)
當(dāng)數(shù)據(jù)往上/下滾動(dòng)的時(shí)候,我們需要復(fù)用和移動(dòng)DOM中的元素褒颈,使其能根據(jù)我們算出的高度顯示在viewport中柒巫,一般情況會(huì)使用position:relative + top來進(jìn)行元素垂直方向的偏移,但是我們知道當(dāng)采用top屬性谷丸,它會(huì)使UI Reflow,性能不是很好堡掏,所以我們采用CSS3中的transform變形屬性來移動(dòng),其優(yōu)點(diǎn)是不會(huì)是UI重新的Reflow和Repaint.
transform - translateY的特點(diǎn)如下刨疼,
(1)范圍:適用所有的HTML標(biāo)簽元素
(2)它是指Y軸(垂直軸)方向的移動(dòng)泉唁,單位可以是px,em或百分比等
(3)當(dāng)y為正時(shí)鹅龄,表示元素在垂直方向向下移動(dòng);
當(dāng)y為負(fù)時(shí)亭畜,表示元素在垂直方向向上移動(dòng)扮休,跟我們的數(shù)學(xué)坐標(biāo)系不同
而
(4)性能優(yōu)化:元素移動(dòng),不會(huì)引起Reflow和Repaint
所以我們?cè)诔跏蓟?跟新要渲染數(shù)據(jù)的時(shí)候拴鸵,可以為其綁定translateY的值
(translateY=itemIndex * itemHeight)玷坠,如,
NK6 Virtual scrolling - 列表渲染優(yōu)化
我們來比較下數(shù)組跟新的兩種方式:變異方法和替換數(shù)組如下
即變異方式和替換數(shù)組方式中的索引值替換劲藐,會(huì)復(fù)用需要更改的DOM元素侨糟,
而數(shù)組全新賦值方式則會(huì)復(fù)用渲染整個(gè)列表(DOM元素移位),
當(dāng)滾動(dòng)元素的時(shí)候瘩燥,數(shù)組全新賦值節(jié)點(diǎn)渲染情況如下秕重,
而局部跟新節(jié)點(diǎn)的渲染取下,
所以當(dāng)我們用translateY屬性來進(jìn)行元素位置移動(dòng)的時(shí)候厉膀,
即使元素插入DOM的位置不是按順序排列的溶耘,但是translateY能確保其元素它在垂直方向的距離如下圖,
紅色區(qū)域代表當(dāng)鼠標(biāo)往下滾動(dòng)的時(shí)候,需要跟新的DOM元素
黃色區(qū)域則代表不需要重新渲染
所以針對(duì)列表的優(yōu)化渲染服鹅,建議不要對(duì)數(shù)組全新賦值凳兵,可以考慮用數(shù)組替換 + 數(shù)組變異的方式來復(fù)用已有的節(jié)點(diǎn)。如果前后數(shù)組變化完全不相等的話企软,可以直接使用數(shù)組全新賦值方式庐扫。
彩蛋: 使用AVA成為該項(xiàng)目的測(cè)試驅(qū)動(dòng)框架,整個(gè)過程采用的是TDD(測(cè)試驅(qū)動(dòng)開發(fā))來實(shí)踐數(shù)據(jù)移動(dòng)的功能模塊仗哨,例如形庭,
當(dāng)要計(jì)算一個(gè)minDomItemHeight的時(shí)候,其測(cè)試用例如下厌漂,
而功能實(shí)現(xiàn)塊:
當(dāng)要計(jì)算一個(gè)maxDomItemHeight的時(shí)候萨醒,其測(cè)試用例如下,
當(dāng)要計(jì)算一個(gè)minDomItemHeight和maxDomItemHeight的時(shí)候苇倡,其測(cè)試用例只需要一個(gè)來驗(yàn)證就行富纸,其結(jié)果如下,
而功能實(shí)現(xiàn)
編輯于 2019-03-20
Vue.js