當今天有大量資料要render出來時,舉例如果有一千筆資料要被呈現出來,但正常viewport不會長到可以顯示一千筆資料,我們會在container加上 overflow: 'scroll'讓container可以符合使用者體驗地去顯示資料,因此我們真正會顯示給使用者的只有在container可視範圍內的資料,也代表其他不在可視範圍的DOM是沒有作用但吃資源,況且在形成DOM tree時需要花上時間去處理一千筆dom。進而導致以下幾種問題:

  • 載入時被blocked時間加長(處理DOM tree以及之後的process)
  • 不必要的dom佔據記憶體
  • 每個DOM太吃資源的情況下會導致FPS降低
  • 若每個元素裡都有掛載圖片,會花一堆時間去下載全部資源(可以用lazy load),但載進來後無非又是多更多不必要資源
  • ...

在這情況下可優化的方式很多種,其中就是今天的主角: Virtualized List

原理

當滾動時,透過計算可視範圍內會有哪些元素去渲染元素,也稱作 Windowing。

virtualized list concept

以上圖為例,若List中有超過一千筆資料,而現在可視範圍內只需要 ID 1000 ~ 1005,那我們只需要這幾個元素,其他移除。

實作

在不考慮Dynamic height元素下,簡單實作一個Virtualized List會需要以下幾個條件去做計算:

  • 元素數量 (constant): itemCount (from props)
  • 元素高度 (constant): itemHeight (from props)
  • 可視範圍高度: listHeight (from props)
  • 可視範圍的scrollTop (from scroll event)

Container

我們需要兩層Container,最外層掌管scroll行為,內層為渲染list,內層的高度為 itemCount * itemHeight

<div className="virtualizedList__wrapper" style={{ overflow: "scroll", height: listHeight }} > <div className="virtualizedList__inner" style={{ position: "relative", height: innerHeight }} > </div> </div>

現在元素範圍

已過去的元素index:

const pastIndex = Math.max( Math.floor(scrollTop / itemHeight) - 1, -1 );

若元素index為 <= pastIndex,我們不渲染。

未來的元素index:

const futureIndex = Math.min( Math.ceil((scrollTop + listHeight) / itemHeight), itemCount );

若元素index為 >= futureIndex,我們不渲染。

渲染元素

我們需要一個prop renderComponent讓我們知道怎麼渲染元素。 renderComponent: (index: number, style: CSSProperties) => ReactElement

在scroll event發生時,根據 pastIndex < index < futureIndex 的範圍去呼叫 renderComponent 在渲染進內層 container,同時改變元素的位移。 因為內層container被scroll,而我們只渲染可視範圍內的元素,若直接把元素放進容器而不位移,元素會從頂層渲染,這時當scrollTop不等於0時,元素會跑出可視範圍。

const temp = []; for (let i = pastIndex + 1; i < futureIndex; i++) { temp.push( renderComponent(i, { position: "absolute", transform: `translateY(${i * itemHeight}px)`, width: "100%" }) ); } setItems([...temp]); // items為state,是可視範圍內的元素

這裡使用 transform: translateY 去位移,使用GPU計算資源。

這下清楚了,直接上code。 code.png

會發現我加了一些小細節:

  • buffer: 在前後提前渲染一些元素,除了提升UX,也可以預載資源
  • throttle: scroll 好夥伴,避免密集運算。

CleanShot 2023-06-13 at 23.33.53.gif CleanShot 2023-06-13 at 23.46.55.gif

CodeSandbox

TODO

  • dynamic height元素的virtualized list