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 中,對於官方推薦方案,用戶反饋則不甚理想。比如:

針對上述問題,官方均回覆以用戶自行解決,因此:

按照 AntD 文檔使用虛擬化進行配置,雖然可以一定程度實現虛擬化,但會引起多處表格功能缺失。

這是目前我們主要亟待解決的問題。

rc-table

由於官方沒有給出很好的虛擬化方案,我們只好深入瞭解 AntD 內部的架構,尋找問題的切入點。經閱讀代碼可以瞭解,在 4.11 的 AntD/Table 代碼架構簡述如下:

至此,我們可以瞭解到,AntD 架構本身其實已經對錶格邏輯進行了一定程度的劃分,與 data 數據的順序及內容無關的邏輯,已經被單獨抽象到了 rc-table 這個庫中。大致上我們可以理解爲下圖:

我們也必須要想清楚:

組內方案陳列

項目組內對於 table 的虛擬化工作產生了多種不同思路:

方案 1:基於 rc-table 不依賴 react-window 和 AntD 實現虛擬化

在不同業務中,我們需要的表格組件功能點並不一致,極端場景下,我們可能只需要使用少量 AntD 的 Feature,且定製化較高。因此我們可以考慮放棄使用 AntD,直接使用 rc-table 並做一定改造。

方案 2:AntD 中截取顯示數據,手動銷燬表格外 dom,手動創建佔位符。

當業務重度依賴 AntD 的各個功能,不能直接移除 AntD。我們只能保留整體框架,找到切入點做部分改造。所以改造 antD 內表格 scroll 事件,使用新的 onScroll 邏輯:

該邏輯下只修改了 AntD 傳入的 data 內容,其餘操作均基於 jsdom 手動完成,影響面小,完成度高。但需要仔細測試對各個表格功能點的影響。

方案 3:重新實現表格

當項目定製化程度較高時,考慮直接放棄 antd。方案 3 保留了 AntD 的 props 定義,以保證從 AntD 遷移時的便利性,隨後利用 react-table 完成大多數功能,虛擬化部分藉助了 react-window,底層開發了全新的具有虛擬化功能的基礎表格。

該方案中引入了 github 上炙手可熱的開源 react hook 框架 react-table,這是個數據邏輯 hook 框架,因爲排序、選擇、等數據邏輯是具有相似性的,所以沒必要重寫,它幫助節省了數據處理的成本,同時也沒有干涉 UI 及虛擬化工作。

改造重點問題覆盤

空白閃爍

在 react-window 的 README 中,可以看到對此問題的描述。當應用虛擬化後,過快的 scroll 動作會導致佔位符尚未更新,只能看到空白內容,需要等少量時間後才加載。當連續快速滾動表格時,呈現不斷閃爍空白內容的狀態。

單元格自適應換行

當單元格文本內容較長時,希望高度可以自適應。此需求乍一看似乎可以利用 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 以及其數據截取。

列虛擬化

總結

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