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