為什麼長列表需要特別處理#
社群媒體例如 Facebook、Instagram 的動態牆,是日常生活中最常見的長列表(Long List)情境之一。實際使用時你會發現它們不會一次載入所有貼文,而是先載一批,等使用者捲到底再追加更多。這背後的優化通常由 Virtualized List 搭配 Lazy Loading 一起完成,本章節先談前者。
如果一個列表有上千筆資料一次全部渲染,就會生出數量同樣龐大的 DOM 節點。再加上每筆貼文本身結構通常不單純,這種做法的代價是顯而易見的:
- 首次載入的白屏時間被拉長。
- 滾動時記憶體用量大幅膨脹。
- 渲染速度跟不上瀏覽器的幀率,畫面卡頓、容易掉幀。
- 嚴重時整個頁面甚至會失去響應。
桌面瀏覽器尚且如此,手機瀏覽器只會更糟。長列表的優化幾乎是必修課題。
Virtualized List 的核心觀念#
Virtualized List(虛擬滾動,Virtual Scrolling)的概念聽起來很玄,其實一句話就講得完:
紀錄所有列表項目的位置,只渲染目前可視區(viewport)內的項目。當捲動發生時,根據捲動偏移量重新計算應該顯示哪些項目。這個技巧也常被稱作「Windowing」。
舉例來說,假設可視區一次只能容納 6 個 item,那即使整個列表有 1000 筆,畫面上實際存在的 DOM 也只會是當下可視區的那 6 個。當原本被渲染的 item 滑出可視區,就會被卸載;新進入可視區的 item 才會被渲染。React 官方文件本身也建議大型列表採用 Windowing 技術。
既有的解決方案#
主流框架幾乎都有現成的 Virtualized List 套件可用:
- React:react-window、react-virtualized
- Vue:vue-virtual-scroll-list、vue-virtual-scroller
- Svelte:svelte-virtual-list
- React Native:built-in 的 VirtualizedList component
除非有特殊客製需求,沒必要從零打造一個 Virtualized List。但動手做一遍對理解原理非常有幫助,以下會帶讀者實作最簡化的版本。
動手實作極簡版#
為了把焦點放在原理,我們先定下幾個簡化條件:
- 每個 list item 的高度固定。
- 可視區視窗的高度固定。
- 因為兩者都固定,可以用
position: absolute直接根據 index 算出每個 item 在整個長列表中的絕對位置。
實作前需要先掌握三個關鍵數值:
- Inner Height:整個長列表的總高度,等於
itemHeight * itemCount。 - Window Height:可視區的高度,由外部傳入。
- Scroll Top:當前捲動的距離,也就是長列表頂端到可視區頂端的偏移量。
Component 的介面#
我們會做一個叫做 SimpleVirtualizedList 的元件,接受四個屬性:
itemHeight:每個 item 的高度。itemCount:總 item 數量。windowHeight:可視區高度。renderItem:渲染單一 item 的 callback function。
itemHeight * itemCount 可以推算出 Inner Height,windowHeight 由外部給定,scrollTop 則靠 container 的 scroll event 動態取得。
計算可視區的 index 範圍#
有了這些前置資訊,就可以推算可視區內第一個 item 的 index 與最後一個 item 的 index:
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
itemCount - 1,
Math.floor((scrollTop + windowHeight) / itemHeight)
);接著對 [startIndex, endIndex] 範圍內的每個 index 渲染 item,並利用 position: absolute 加上 top = index * itemHeight 把它放到正確位置。
const items = [];
for (let i = startIndex; i <= endIndex; i++) {
items.push(
renderItem({
index: i,
style: {
position: "absolute",
top: i * itemHeight,
height: itemHeight,
},
})
);
}跑起來會怎樣#
假設我們建一個有 2000 個 item 的假資料丟進去,把 itemHeight 設為 40px、windowHeight 設為 400px,那麼可視區同時顯示的 item 永遠是 10 個。從 DevTool 觀察 DOM,會發現不論怎麼滾動,畫面上始終只有那 10 個 item element 存在。
還可以怎麼優化#
最簡版本只是入門,實際的開源套件還處理了非常多細節:
- 支援高度不一致的 item,計算可視區範圍會比較複雜,通常需要建立位置快取。
- 在可視區邊界附近做預渲染,避免使用者快速捲動時看到短暫空白。
- 改用 ResizeObserver 動態追蹤可視區高度的變化。
- 用
transform: translate取代top排版,讓動畫走 GPU 加速、得到更平順的捲動體驗。
Virtualized List 解決了一次渲染大量 DOM 元素的問題,但資料本身仍然得一次性丟給元件。當資料量極大時,光是這份資料本身就會對記憶體造成負擔,這正是下一章 Lazy Loading 要處理的問題。
一個小總結#
Virtualized List 不論是在電商商品列表、社群平台動態牆,或新聞平台都是優化長列表必備的技巧。今天的極簡實作只是讓讀者掌握背後的核心:「不要渲染看不到的東西」。要把它發揮到極致,還需要與 Lazy Loading 搭配使用。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10271764