前端效能優化之列表虛擬化 (virtualization)
2026年5月27日
在前端效能優化的各種手段中,列表虛擬化 (list virtualization) 是很常用來處理長列表資料的方法。
不論是通知列表、聊天訊息、搜尋結果、商品清單,或後台常見的資料表,剛開始使用的時候,資料只有幾十筆,滑起來多半會很順暢。但當資料變成幾千筆、幾萬筆之後,功能明明沒有壞,資料也正確顯示,可是頁面就開始變慢。滾動的時候會有點卡,滑到中間甚至會有一種畫面跟不上手指的感覺。
在遇到這種問題時,可能有多個解決的切入角度,而虛擬化是其中一個。
虛擬化在解決什麼問題? 為什麼需要虛擬化?
要了解虛擬化,需要先從這個方法解決的問題談起。當今天瀏覽器像後端拿資料,不是拿到資料就結束,而是要進一步把資料變成畫面,而這件事本身是有成本的。
如果畫面上有一萬筆資料,我們就真的渲染一萬個項目,那瀏覽器要建立很多 DOM 節點、計算樣式、安排版面、處理繪製,還要在使用者滾動時持續管理這些東西。問題是,使用者同一時間可能只看得到十幾列,最多幾十列。
明明使用者在每一個當下只需要看在畫面中十幾列資料,卻要瀏覽器同時管理一萬列資料的 DOM,要知道每個元素在哪裡、長什麼樣子、尺寸是多少、哪些地方需要重繪,造成瀏覽器的額外負擔,顯然並不理想。
這邊有個重要的觀念需要記在心上,資料存在瀏覽器的記憶體裡,跟資料被渲染成 DOM,有不同的成本。資料在記憶體裡可能只是陣列中的一筆物件,但一旦變成畫面,它就進入瀏覽器的渲染流程。這個流程包含很多工作:建立元素、計算樣式、排版、繪製、合成。單筆資料很便宜,但一萬筆一起來,就會變成使用者感受到的卡頓。
對此,虛擬化的存在就是為了解決這個問題。它的核心想法不是讓資料消失,也不是讓列表變短,而是畫面上只渲染使用者現在看得到,或快要看得到的那一小段內容。使用者感覺自己在滑一個完整列表,但瀏覽器實際上每一刻只管理其中一小段。
虛擬化具體在做什麼?
不過,具體來說,虛擬化是如何做到減少瀏覽起要維護的 DOM 數量呢?
想像畫面上有一個高度 600 像素的列表容器。每一列高度 60 像素,所以使用者同一時間大概看得到 10 列。實務上為了讓滾動更順,我們可能會在上下多渲染幾列當作緩衝,所以畫面實際存在的項目可能是 15 到 20 列。
就算整份資料有一萬筆,虛擬化也只會讓瀏覽器在當下管理這十幾二十列。使用者往下滑,原本上方離開可視區域的項目會被移除;新的項目進入可視區域,就被渲染出來。使用者感覺自己在瀏覽完整列表,但 DOM 裡其實只存在目前附近那一小段。
這是為什麼虛擬化有時也會被稱為視窗化(windowing),因為這就像拿一個小框框在一大卷資料上移動,框框移到哪裡,畫面就渲染哪裡。
要做到虛擬化,實作上通常要做幾件事:
- 要知道總共有多少項目,這樣才能算出整個列表應該有多高。否則捲軸會不正常,使用者也不知道下面還有多少內容。
- 要知道目前滾動到哪裡,才能算出現在應該渲染第幾筆到第幾筆。
- 要把這些被渲染出來的項目放在正確的位置。否則明明資料是第 500 筆,卻出現在畫面最上方,看起來就會錯亂。
所以從實作角度看,虛擬化核心是對可視區域的管理。
推薦的虛擬化套件
當提到虛擬化的套件,目前在社群中最熱門的,莫過於 TanStack Virtual。TanStack Virtual 除了使用上簡單,更重要地勢跨框架的支援,不只支援 React,也支援 Vue、Svelte、Solid、Lit、Angular 等框架。對團隊來說,這種概念可以跨專案延續,不會只綁在某個單一 React 元件 API 上。
簡化後,使用 TanStack Virtual 的概念大概會像這樣:
function MessageList({ messages }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 64,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const message = messages[virtualItem.index];
return (
<div
key={message.id}
style={{
position: "absolute",
transform: `translateY(${virtualItem.start}px)`,
height: virtualItem.size,
width: "100%",
}}
>
{message.title}
</div>
);
})}
</div>
</div>
);
}
可以看到,原本複雜的虛擬化,被用簡潔的 API 呈現出來。在上面的範例中,count 告訴虛擬化工具,總共有幾筆資料。getScrollElement 則是讓虛擬化工具知道哪個容器在負責滾動。estimateSize 則是每一列大概多高。
getTotalSize 用來撐出整個列表的總高度,讓捲軸看起來像真的。而 getVirtualItems 則是目前真正需要渲染的那些項目。
透過虛擬化,畫面看起來像完整列表,但實際 DOM 只包含目前可視區域附近的項目。換句話說,虛擬化不是讓列表變短,而是讓瀏覽器永遠只處理使用者當下需要看的那一段。
不是所有列表都要虛擬化
雖然上面提了虛擬化的優點,也介紹了 TanStack Virtual 這個簡單好用的工具;但在實務上,不推薦看到長列表就加虛擬化。
虛擬化本質上是在用複雜度換效能。它讓 DOM 數量下降,但也會帶來一些額外成本。從軟體工程角度來看,重要的不是能不能用,而是這個場景值不值得用。假如今天資料量其實不大。如果列表最多就幾十筆、一百筆,每一列也很簡單,那直接渲染通常就很好。這時候加虛擬化,可能只是讓程式碼變難懂。原本一個 map 就能完成的畫面,突然多了滾動容器、估算高度、絕對定位、測量尺寸,除錯成本反而變高。
此外,如果畫面功能依賴完整 DOM,也不適合用虛擬化。舉例來說,如果使用者期待用瀏覽器的頁面搜尋功能搜尋整個列表,或需要一次列印完整內容,或某些輔助科技情境需要更完整的內容結構。因為虛擬化只把可視區域附近的項目放進 DOM,所以畫面外的內容可能根本還不存在於頁面元素裡,這時加上虛擬化可能會導致 bugs。
因此,在判斷要不要虛擬化時,可以思考
- 這個列表的 DOM 數量是否真的可能變大?
- 使用者是否會頻繁滾動,而且滾動體感很重要?
- 加入虛擬化後,複雜度是否比目前的效能問題更值得?
如果答案都是肯定的,虛擬化可能是合理選擇。但如果資料量不大、畫面很單純、效能沒有明顯問題,那反而推薦保持簡單。
閱讀更多
如果前端效能優化這個主題感興趣,我們在 E+ 成長計畫中的主題文有談到更多具體實務的方法。對更深入了解這個主題,以及其他前後端開發、軟體工程、AI 工程主題感興趣的讀者,歡迎加入 E+ 成長計畫一起成長 (連結)。