
在小程序開發(fā)場(chǎng)景中,列表是最基礎(chǔ)且使用頻率最高的組件,當(dāng)列表數(shù)據(jù)量達(dá)到數(shù)百條及以上時(shí),原生普通滾動(dòng)列表會(huì)出現(xiàn)肉眼可見的性能問題,這也是虛擬列表方案誕生的核心原因。想要弄懂虛擬列表,首先需要明確原生列表卡頓、加載慢、滑動(dòng)掉幀的底層根源。
小程序視圖層基于原生渲染引擎搭建,每一條列表項(xiàng)都會(huì)對(duì)應(yīng)獨(dú)立的DOM節(jié)點(diǎn),同時(shí)附帶樣式、綁定事件、內(nèi)部子節(jié)點(diǎn)等附屬渲染資源。原生列表的渲染邏輯為:一次性渲染全部數(shù)據(jù)對(duì)應(yīng)的DOM節(jié)點(diǎn),不會(huì)隨著滾動(dòng)動(dòng)態(tài)增減節(jié)點(diǎn)。即便部分列表項(xiàng)已經(jīng)滾動(dòng)到屏幕可視區(qū)域之外,脫離用戶視野,對(duì)應(yīng)的DOM節(jié)點(diǎn)依舊會(huì)常駐視圖層,持續(xù)占用內(nèi)存與渲染線程資源。
具體性能損耗分為三個(gè)維度:第一是內(nèi)存占用,海量DOM節(jié)點(diǎn)會(huì)持續(xù)占用視圖層內(nèi)存,頁面停留時(shí)間越長(zhǎng),內(nèi)存堆積越嚴(yán)重,極易觸發(fā)小程序內(nèi)存回收機(jī)制,導(dǎo)致頁面閃退、白屏;第二是首屏渲染耗時(shí),頁面初始化時(shí)需要遍歷全部數(shù)據(jù)、創(chuàng)建全部節(jié)點(diǎn)、計(jì)算全部節(jié)點(diǎn)布局,數(shù)據(jù)量越大,首屏白屏加載時(shí)間越長(zhǎng);第三是滾動(dòng)流暢度問題,滑動(dòng)頁面時(shí),渲染線程需要持續(xù)監(jiān)聽全部節(jié)點(diǎn)的位置變化,進(jìn)行重排與重繪,大量節(jié)點(diǎn)會(huì)直接拉高主線程壓力,出現(xiàn)滑動(dòng)卡頓、觸摸延遲、滾動(dòng)慣性失效等問題。
行業(yè)內(nèi)常規(guī)的分頁加載、懶加載方案只能緩解部分首屏壓力,無法解決可視區(qū)外冗余DOM節(jié)點(diǎn)常駐的問題,只要數(shù)據(jù)持續(xù)累加,頁面性能依舊會(huì)持續(xù)走低。而虛擬列表是從DOM渲染層面根治長(zhǎng)列表性能問題的最優(yōu)方案,核心思路是只保留屏幕可視區(qū)域內(nèi)需要展示的DOM節(jié)點(diǎn),徹底銷毀可視區(qū)外的節(jié)點(diǎn),將頁面常駐DOM節(jié)點(diǎn)數(shù)量恒定控制在極低范圍,不受總數(shù)據(jù)量影響。
虛擬列表的底層邏輯可以概括為可視區(qū)裁剪+偏移量占位+數(shù)據(jù)動(dòng)態(tài)映射三大核心機(jī)制,無論列表總數(shù)據(jù)量是上千條還是上萬條,頁面中實(shí)際存在的DOM節(jié)點(diǎn)始終維持在「可視區(qū)條數(shù)+上下緩沖條數(shù)」固定數(shù)值,從根源減少DOM節(jié)點(diǎn)數(shù)量。
虛擬列表需要一個(gè)外層滾動(dòng)容器承接滾動(dòng)事件,容器內(nèi)部分為兩層結(jié)構(gòu):第一層是高度等于全部列表總高度的空白占位層,第二層是承載實(shí)際列表項(xiàng)的內(nèi)容渲染層。
空白占位層不會(huì)渲染任何業(yè)務(wù)內(nèi)容,僅通過固定高度撐起整個(gè)滾動(dòng)區(qū)域,模擬完整長(zhǎng)列表的滾動(dòng)高度,保證頁面滾動(dòng)條的長(zhǎng)度、滾動(dòng)范圍和原生完整列表完全一致,用戶滾動(dòng)感知無任何差異。總高度計(jì)算公式為:列表總條數(shù) × 單條列表項(xiàng)固定高度(定高虛擬列表),若是不定高列表,則需要累計(jì)每一條真實(shí)渲染項(xiàng)的高度得出總占位高度。
頁面存在固定尺寸的可視窗口,結(jié)合滾動(dòng)距離可以實(shí)時(shí)計(jì)算出當(dāng)前處于可視區(qū)內(nèi)的數(shù)據(jù)下標(biāo)范圍。滾動(dòng)容器監(jiān)聽滾動(dòng)事件后,實(shí)時(shí)獲取垂直滾動(dòng)偏移量,結(jié)合單條列表項(xiàng)高度,計(jì)算出:當(dāng)前滾動(dòng)起始下標(biāo)、當(dāng)前滾動(dòng)結(jié)束下標(biāo)。
系統(tǒng)只會(huì)截取起始下標(biāo)到結(jié)束下標(biāo)之間的數(shù)據(jù)進(jìn)行DOM渲染,其余所有下標(biāo)對(duì)應(yīng)的數(shù)據(jù)均不生成DOM節(jié)點(diǎn)。為了避免滾動(dòng)過程中出現(xiàn)空白屏,需要在可視區(qū)上下各增加一定數(shù)量的緩沖節(jié)點(diǎn),提前渲染即將進(jìn)入可視區(qū)的列表項(xiàng),保證快速滑動(dòng)時(shí)頁面無空白斷層。
僅僅裁剪數(shù)據(jù)還無法實(shí)現(xiàn)滾動(dòng)效果,需要通過CSS transform位移屬性,動(dòng)態(tài)改變內(nèi)容渲染層的垂直偏移量。偏移量等于「當(dāng)前起始下標(biāo) × 單條列表項(xiàng)高度」,讓渲染出來的少量列表項(xiàng),始終精準(zhǔn)貼合當(dāng)前滾動(dòng)位置,視覺上和完整長(zhǎng)列表滾動(dòng)效果完全一致。
小程序受雙線程架構(gòu)限制(邏輯層、視圖層分離,通信存在延遲),不能直接照搬Web端虛擬列表方案,需要適配小程序原生scroll組件能力,分為定高虛擬列表(簡(jiǎn)單易實(shí)現(xiàn),通用性強(qiáng))和不定高虛擬列表(適配動(dòng)態(tài)內(nèi)容列表,復(fù)雜度更高)兩種實(shí)現(xiàn)方式,下文以最常用的定高虛擬列表拆解完整開發(fā)流程。
摒棄原生wx:for循環(huán)渲染全部列表數(shù)據(jù)的寫法,采用小程序scroll-view作為外層滾動(dòng)容器,結(jié)構(gòu)分為三層:外層滾動(dòng)容器、占位高度盒子、實(shí)際內(nèi)容渲染盒子。
外層scroll-view開啟垂直滾動(dòng),禁止原生滾動(dòng)回彈避免偏移計(jì)算異常;占位盒子綁定動(dòng)態(tài)計(jì)算的總列表高度,撐起滾動(dòng)區(qū)域;內(nèi)容盒子通過transform做垂直位移,內(nèi)部?jī)H循環(huán)渲染當(dāng)前可視區(qū)+緩沖區(qū)的少量數(shù)據(jù),徹底減少wx:for渲染節(jié)點(diǎn)數(shù)量。
在頁面邏輯層定義固定核心變量,支撐全部計(jì)算邏輯:
itemHeight:?jiǎn)螚l列表項(xiàng)固定高度,提前統(tǒng)一樣式固定值;
visibleCount:屏幕可視區(qū)域內(nèi)可展示的列表項(xiàng)數(shù)量,由滾動(dòng)容器高度/單條項(xiàng)高度自動(dòng)計(jì)算;
bufferCount:上下緩沖條數(shù),一般設(shè)置4-6條,平衡空白屏和渲染性能;
scrollTop:實(shí)時(shí)滾動(dòng)垂直偏移量,由scroll-view滾動(dòng)事件獲取;
startIndex、endIndex:當(dāng)前渲染數(shù)據(jù)的起始、結(jié)束下標(biāo);
renderList:實(shí)際頁面渲染的切片數(shù)據(jù),永遠(yuǎn)為固定條數(shù)。
綁定scroll-view的scroll滾動(dòng)事件,每次滾動(dòng)觸發(fā)時(shí)實(shí)時(shí)獲取scrollTop值,同步更新起始下標(biāo):startIndex = Math.floor(scrollTop / itemHeight) - bufferCount。為了防止起始下標(biāo)小于0出現(xiàn)負(fù)數(shù),需要做邊界兜底,最小起始下標(biāo)固定為0。
結(jié)束下標(biāo)計(jì)算公式:endIndex = startIndex + visibleCount + bufferCount * 2。通過起始和結(jié)束下標(biāo),從完整源數(shù)據(jù)中截取對(duì)應(yīng)區(qū)間的數(shù)據(jù)賦值給renderList,頁面僅渲染該切片數(shù)據(jù)。
根據(jù)起始下標(biāo)計(jì)算內(nèi)容容器的垂直偏移距離,偏移值 = startIndex * itemHeight,通過內(nèi)聯(lián)樣式綁定transform:translateY(${偏移值}rpx),讓渲染的列表項(xiàng)跟隨滾動(dòng)位置實(shí)時(shí)移動(dòng),填補(bǔ)上方空白區(qū)域,保證視覺滾動(dòng)連貫。
小程序邏輯層和視圖層分離,滾動(dòng)事件高頻觸發(fā)會(huì)造成兩層頻繁通信,引發(fā)延遲和卡頓。需要增加節(jié)流函數(shù)限制滾動(dòng)計(jì)算頻率,將計(jì)算頻率控制在16ms一次(和瀏覽器一幀渲染時(shí)長(zhǎng)對(duì)齊),避免主線程被大量計(jì)算邏輯阻塞;同時(shí)避免在滾動(dòng)事件中執(zhí)行setData高頻更新數(shù)據(jù),合并多次滾動(dòng)計(jì)算結(jié)果,減少視圖層和邏輯層的數(shù)據(jù)通信次數(shù)。
實(shí)際業(yè)務(wù)中大部分列表項(xiàng)內(nèi)容不固定,圖片、文字長(zhǎng)度變化會(huì)導(dǎo)致列表項(xiàng)高度動(dòng)態(tài)變化,定高虛擬列表不再適用,需要引入節(jié)點(diǎn)高度緩存機(jī)制實(shí)現(xiàn)不定高虛擬列表。
核心優(yōu)化邏輯:首次渲染列表項(xiàng)后,通過小程序createSelectorQuery獲取每一個(gè)已渲染列表項(xiàng)的真實(shí)dom高度,存入本地緩存數(shù)組;后續(xù)滾動(dòng)計(jì)算起始下標(biāo)、總占位高度時(shí),不再使用固定高度,而是讀取緩存內(nèi)每一條數(shù)據(jù)的真實(shí)高度進(jìn)行累加計(jì)算。
同時(shí)需要維護(hù)累計(jì)高度數(shù)組,記錄每一個(gè)下標(biāo)對(duì)應(yīng)的累計(jì)總高度,通過二分查找算法替代遍歷查找,根據(jù)滾動(dòng)距離快速匹配當(dāng)前起始渲染下標(biāo),降低海量數(shù)據(jù)下的下標(biāo)查找耗時(shí)。不定高方案實(shí)現(xiàn)復(fù)雜度更高,但可以適配所有動(dòng)態(tài)列表場(chǎng)景,是生產(chǎn)環(huán)境更通用的虛擬列表方案。
根源是滾動(dòng)事件通信延遲,視圖層滾動(dòng)位置更新快于邏輯層數(shù)據(jù)切片更新。解決方案:合理增大緩沖條數(shù),同時(shí)開啟scroll-view的被動(dòng)滾動(dòng)監(jiān)聽,提升滾動(dòng)事件響應(yīng)速度,禁止?jié)L動(dòng)過程中額外的復(fù)雜業(yè)務(wù)邏輯。
原生下拉刷新和觸底加載基于完整列表高度計(jì)算,虛擬列表僅有少量DOM節(jié)點(diǎn),會(huì)導(dǎo)致觸底時(shí)機(jī)提前或延后。解決方案:監(jiān)聽數(shù)據(jù)源變化,數(shù)據(jù)新增或清空后,重新計(jì)算總占位高度、重置高度緩存,同步更新滾動(dòng)容器布局。
圖片異步加載會(huì)改變列表項(xiàng)真實(shí)高度,破壞高度緩存準(zhǔn)確性。解決方案:圖片標(biāo)簽提前設(shè)置固定寬高占位,圖片加載完成后重新獲取當(dāng)前節(jié)點(diǎn)高度,更新本地高度緩存,同步修正內(nèi)容容器偏移量。
滾動(dòng)監(jiān)聽事件、節(jié)點(diǎn)查詢請(qǐng)求若未銷毀,會(huì)持續(xù)占用頁面內(nèi)存。解決方案:頁面卸載時(shí),清除滾動(dòng)節(jié)流定時(shí)器、清空高度緩存數(shù)組、取消未完成的節(jié)點(diǎn)查詢請(qǐng)求,釋放全部監(jiān)聽資源。
優(yōu)化方案 |
DOM節(jié)點(diǎn)數(shù)量 |
內(nèi)存占用 |
滑動(dòng)流暢度 |
實(shí)現(xiàn)成本 |
|---|---|---|---|---|
原生一次性渲染 |
等于總數(shù)據(jù)量 |
極高 |
差 |
極低 |
分頁懶加載 |
持續(xù)累加 |
持續(xù)升高 |
一般 |
低 |
虛擬列表 |
固定恒定值 |
極低 |
優(yōu)秀 |
中高 |
從對(duì)比結(jié)果可以看出,分頁懶加載只能延緩性能惡化,無法根治問題,而虛擬列表從DOM渲染底層解決長(zhǎng)列表痛點(diǎn),無論數(shù)據(jù)量多大,頁面性能始終保持穩(wěn)定。只有在數(shù)據(jù)量小于100條的短列表場(chǎng)景下,才無需使用虛擬列表,避免過度優(yōu)化增加代碼復(fù)雜度。
小程序虛擬列表的核心本質(zhì)并不是優(yōu)化滾動(dòng)邏輯,而是控制頁面常駐DOM節(jié)點(diǎn)數(shù)量,通過占位層模擬完整列表滾動(dòng)高度,通過數(shù)據(jù)切片只渲染可視區(qū)域內(nèi)容,通過CSS位移實(shí)現(xiàn)視覺滾動(dòng)效果,完美規(guī)避小程序雙線程架構(gòu)下長(zhǎng)列表的渲染、通信、內(nèi)存三大性能問題。
定高虛擬列表適合樣式統(tǒng)一、高度固定的列表,開發(fā)成本低、運(yùn)行穩(wěn)定;不定高虛擬列表適配全部動(dòng)態(tài)內(nèi)容場(chǎng)景,依靠高度緩存和二分查找彌補(bǔ)動(dòng)態(tài)高度帶來的計(jì)算偏差。在實(shí)際開發(fā)中,只要理清滾動(dòng)偏移量、數(shù)據(jù)下標(biāo)、容器位移三者的聯(lián)動(dòng)關(guān)系,同時(shí)做好小程序雙線程通信節(jié)流、資源銷毀、邊界值兜底等適配處理,就能自主實(shí)現(xiàn)高性能、無依賴的原生虛擬列表,無需引入第三方組件庫,徹底解決小程序長(zhǎng)列表所有卡頓、白屏、內(nèi)存溢出問題。