Table 組件虛擬化實踐
前言
列表及表格的虛擬優化不是個新鮮的課題,近期,團隊發現:業界對於 table 虛擬化竟然沒有一個相對一勞永逸的解決方案。這是爲什麼?又該如何解決?在本文中,我們會循序漸進的介紹在 React+AntDesign 技術棧下,團隊內部對 Table 組件虛擬化的不同實踐思路,分析可能遇到的難點問題。
虛擬化問題闡述
在列表頁、瀑布流、Select 組件中,我們都有可能遇到渲染大數量級列表的場景。在過去,我們總結了一套卓有成效的辦法:對於列表,我們通過計算,保證在滾動視窗時,每次只渲染部分元素。這樣既減少了首屏壓力,也保證長時間加載也不會有更多的性能負擔,可以滿足上述大部分場景的高性能優化需求。具體做法上,我們利用已知的固定行高和滾動偏移量,計算出滾動到的表格行索引,只渲染出有限視窗內所需要的元素,,並對列表進行相應設置,簡述過程如下:
這一方案是廣泛被大多數開發人員探討的。我推薦參考淺說虛擬列表的實現原理 [1],他對問題的思路,實現,均有比較詳盡的描述。如今,我們的場景來到了 AntDesign 的 Table 組件。我們面對十分類似的問題:業務當中,**Table **涉及 1000 + 行& 100 + 列數量級別的渲染時,由於單元格內也具有一定的複雜邏輯,因此頁面渲染時長往往需要卡頓 **5000ms **前後的時間。這顯然是不能夠接受的。實際上,從長列表到 Table 組件,我們的列表無非是從一維軸線上升到了二維平面。因此,所謂表格虛擬化,無非就是希望表格可以實現:在兼容 Table 現有功能的情況下,實現表格只渲染視窗平面內容,對於視窗外的行、列予以隱藏。
整理現狀
AntDesign
我們內部的 React + umi + AntDesign 技術棧是目前前端界較爲常見的一種架構基礎。AntD 作爲業界 react 常用組件庫,它提供的 Table 組件,能夠方便的幫我們解決諸多常見需求,包括並不僅限於:行選擇、行展開、行篩選、數據分頁、列固定... 目前 AntDesign 有 AntD@3.26 和 AntD@4.11 兩大版本,後者經過一定的重構和優化,是官方推薦的最新內容。AntDesign3 文檔中已經刪除了對於虛擬化支持的 demo(實際也可以使用),而在 AntDesign4 文檔中,能看到官方推薦用戶以接入 react-window 的方式解決虛擬化表格問題。
從官方的 Demo 來看,AntD 提供了一個 components 屬性,通過傳入一個對象,在其 body 屬性中給到一個 ReactWindow 提供的虛擬化組件,以實現需求
...
// VariableSizeGrid is a component provided by react-window
const renderVirtualList = (rawData, { scrollbarSize, ref, onScroll })=>
<VariableSizeGrid
{...props}
/>
...
<Table
{...props}
class
columns={mergedColumns}
pagination={false}
components={{
// overwrite the body set by AntD
body: renderVirtualList,
}}
/>
...
ReactWindow
react-window 是一個廣受歡迎的用於解決表格虛擬化問題的開源代碼庫,它產出了不同特性的虛擬化組件,以便用戶聚焦不同虛擬場景的問題解決。
react-window 的原理並不複雜,主要就是本文在虛擬化問題闡述中對於虛擬化要求內容的實現。主要是通過監控 onScroll,動態調整表格橫軸偏移量,節選適當數量的部分數據,進行可視區內容渲染。
他的前身是 react-virtualized。經過了重構和升級,作者對 table 和 list 兩種不同的場景做了更好的抽象,通過重用共通部分的邏輯,實現了更好的性能,代碼打包大小也減少到了原先的 20%。
接入時遇到的問題
看上去,AntD 已經給出了一套虛擬化方案。但稍稍深入調查不難發現,在 Github 的 Issue 中,對於官方推薦方案,用戶反饋則不甚理想。比如:
-
在問題#21022[2] 中提出,使用 AntD3 後,展開、多選等功能點缺失。
-
在問題#20339[3] 中提出,使用 AntD4 後,大多數表格功能點都存在缺失。
針對上述問題,官方均回覆以用戶自行解決,因此:
按照 AntD 文檔使用虛擬化進行配置,雖然可以一定程度實現虛擬化,但會引起多處表格功能缺失。
這是目前我們主要亟待解決的問題。
rc-table
由於官方沒有給出很好的虛擬化方案,我們只好深入瞭解 AntD 內部的架構,尋找問題的切入點。經閱讀代碼可以瞭解,在 4.11 的 AntD/Table 代碼架構簡述如下:
-
在 AntDesign/Table 中:
-
初始化表格大小和行列內容
-
初步整理事件和數據
-
排序、分頁、過濾等功能對 data 和 column 的計算邏輯
-
調用 rc-table 依賴
-
在 rc-table 中:
-
註冊各類行列單元格事件
-
完成各種樣式需求,如列固定,行展開
-
完成渲染
至此,我們可以瞭解到,AntD 架構本身其實已經對錶格邏輯進行了一定程度的劃分,與 data 數據的順序及內容無關的邏輯,已經被單獨抽象到了 rc-table 這個庫中。大致上我們可以理解爲下圖:
我們也必須要想清楚:
-
這三層邏輯,我們分別對他們做保留,改造,還是替換?
-
如保留,如何解決引入虛擬化邏輯後,虛擬化邏輯對現有框架造成的影響?
-
如不保留,新的框架如何選擇?
組內方案陳列
項目組內對於 table 的虛擬化工作產生了多種不同思路:
方案 1:基於 rc-table 不依賴 react-window 和 AntD 實現虛擬化
- 實現思路:
在不同業務中,我們需要的表格組件功能點並不一致,極端場景下,我們可能只需要使用少量 AntD 的 Feature,且定製化較高。因此我們可以考慮放棄使用 AntD,直接使用 rc-table 並做一定改造。
-
實現方式:
-
fork 一份穩定版本 rc-table 放入代碼庫內
-
按照虛擬化原理對 rc-table 完成必要改造
-
自己實現排序、選擇、列固定等上層功能點
-
方案優劣:
-
丟失了 AntD 的功能基礎。
-
不再需要自己實現基礎的表格呈現,這部分由 rc-table 完成,我們只需要針對虛擬化對滾動事件做少量改造。
-
外部功能 feature 方便自定製。
方案 2:AntD 中截取顯示數據,手動銷燬表格外 dom,手動創建佔位符。
- 實現思路:
當業務重度依賴 AntD 的各個功能,不能直接移除 AntD。我們只能保留整體框架,找到切入點做部分改造。所以改造 antD 內表格 scroll 事件,使用新的 onScroll 邏輯:
-
判定 dom 行位置,計算 index
-
setState 動作完成後,手動銷燬超出視窗的 dom 內容,同時創建等高空白區域,以維護滾動條位置。
-
對 data 數據內容進行裁切並更新,保證視窗內數據的正確性。
該邏輯下只修改了 AntD 傳入的 data 內容,其餘操作均基於 jsdom 手動完成,影響面小,完成度高。但需要仔細測試對各個表格功能點的影響。
-
方案優劣:
-
需要小心處理改方案對列固定、行展開等特性的影響。
-
由於無法提前獲取行高,暫不支持直接定位,等價方案是需要先搜索到對應的數據,然後將結果裝入 InfinityTable
-
向前兼容 Antd Table 的配置參數,只需要新增少數 props,改造成本極低
-
直接操控 dom,開發工作除了數據截取,其餘大部分不依賴 AntD/rcTable 代碼,性能有保障。
方案 3:重新實現表格
- 實現思路
當項目定製化程度較高時,考慮直接放棄 antd。方案 3 保留了 AntD 的 props 定義,以保證從 AntD 遷移時的便利性,隨後利用 react-table 完成大多數功能,虛擬化部分藉助了 react-window,底層開發了全新的具有虛擬化功能的基礎表格。
- 框架選擇理由
該方案中引入了 github 上炙手可熱的開源 react hook 框架 react-table,這是個數據邏輯 hook 框架,因爲排序、選擇、等數據邏輯是具有相似性的,所以沒必要重寫,它幫助節省了數據處理的成本,同時也沒有干涉 UI 及虛擬化工作。
-
方案優劣:
-
表格基礎實現的成本高,基礎 table 需要結合 react-table 的 api 重新實現,遇到各種坑都需要自己踩
-
深入改造,重構程度高,方便後續做任何擴展,方便性能優化
-
利用開源產品實現原 AntD 的開發內容,節省一定成本
改造重點問題覆盤
空白閃爍
- 問題陳述:
在 react-window 的 README 中,可以看到對此問題的描述。當應用虛擬化後,過快的 scroll 動作會導致佔位符尚未更新,只能看到空白內容,需要等少量時間後才加載。當連續快速滾動表格時,呈現不斷閃爍空白內容的狀態。
-
問題解決:
-
目前沒有較好解決辦法,增加預載區大小,且優化單元格渲染內容,可以減緩問題嚴重性。
-
有同學提出通過監控 scroll 時候的 speed 動態調整預載區大小,不失爲一個沒實踐的思路。
單元格自適應換行
- 問題陳述:
當單元格文本內容較長時,希望高度可以自適應。此需求乍一看似乎可以利用 react-window 的不定高組件解決,但實際上,該組件需要提供一個高度函數:
// Returns the size of a item in the direction being windowed.
// For vertical lists, this is the row height.
// For horizontal lists, this is the column width.
itemSize: (index: number) => number
當文本變化時,我們也需要 render 後才能獲取每一行的高度,所以這個 api 並沒有想象中美好。該問題還是必須藉助二次渲染後,通過 ref 拿 dom 節點才能解決,但這樣會拖慢性能,因此考量後,對此方案不予支持
列固定
- 問題描述
在方案 3 中,由於使用了 div 的 flex 排版而不是原生 table,爲了實現列固定時,我們需要使用三個 table 來做固定效果,因此滾動時,我們需要同時改變多個表格的 scrollTop 以及其數據截取。
-
問題解決
-
可以使用一個 state 同步多個 table 之間的 scrollTop,但這種實現有可能因爲性能,產生表格渲染的先後,進而產生三張表對不齊的問題。(AntD 的 Header 與 body 對齊本身也有此問題)
-
AntD 不區分三個 table,而是在一個 table 內利用原生 tr/td,以及 css 的 sticky 特性,部分迴避了該問題,但沒有解決的很好,fixedcolumn 存在的場景下,header 和 body 的同步依舊存有類似問題。
-
可以將三個表格改爲一個表格,然後利用性能最好的 3Dtransform 來解決此問題。下方推薦的開源產品 rsuite-table 比較好的通過此方案解決了問題。
列虛擬化
-
問題描述:
-
由於目前方案專注於行數據量大的場景,因此當遇到表格列數據量大的場景,依舊存在性能問題。
-
如果我們引入 react-window 的 Grid 組件進行列虛擬化,會需要兼容子列,列固定等功能,實際實現比想象中複雜。
-
問題解決:
-
過多的列從產品設計來說是不合理的。建議從產品層面改善數據展示問題。
-
目前暫不支持列虛擬化。
總結
Table 組件虛擬化從原理上來說,是比較單純的。即便不借助 react-window,我們也看到了同學們每個人都可以用自己的思路實現相似的功能需求。
之所以產生方案叢生無法統一的現狀,主要還是由於不同項目對錶格庫的要求不同:有的項目需要低成本,有的項目要求兼容各種不同 feature,有的項目有着繁重的歷史包袱。
而一個新的、面面俱到的 Table 庫,又會有替換成本、穩定性風險等問題。因此,因地制宜地對自己項目進行改造,實現一套適合自己項目的虛擬化方案反而是最快速的,最被大家接受的。
因此,本文從三個不同場景出發,總結了視野可及範圍內,大家解決該問題的不同思路。所謂授人以魚不如授人以漁,希望文中探討的內容,或多或少在提升表格性能及表格虛擬化方向,能給與讀者一些啓發,爲用戶帶來更快更好的體驗。
參考資料
https://github.com/dwqs/blog/issues/70#
https://github.com/ant-design/ant-design/issues/21022
https://github.com/ant-design/ant-design/issues/20339
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/790XQL0qitoiU9wH4Qpjsw