Facebook、Instagram 應該都是我們日常生活中非常依賴的社群媒體了,每天閒來無事就要滑滑動態,看看朋友最近發生了什麼事。不過你有沒有注意到一件事,通常在滑動態的時候不會一次就把所有貼文載入完,而是會先載入一部分,等你滑動到底下後再接著載入更多貼文。這麼做的原因其實也是為了效能的優化,通常這樣的 feature 會由 Virtualized List 搭配 Lazy Loading 來完成,今天我將帶領各位認識 Virtualized List 的觀念,明天則會接著講解 Lazy Loading,廢話不多說,直接進入今天的正題吧!
Virtualized List
,為什麼需要它 ?長列表 Long List,是網站中蠻常見的一個 feature,例如 FB 等社群媒體會有大量的文章列表,然而如果有 1000 篇文章,我們又將這些文章同時渲染的話,就必須生成 1000 個 DOM 節點,更不用說文章結構通常是相對複雜的,像這樣同時渲染數量龐大的元素會有幾個明顯的缺點:
而且這些問題在 Desktop 瀏覽器就會發生了,換作是手機瀏覽器只會讓問題變得更嚴重,因此這種狀況下我們應該優化長列表,提升使用者體驗。
Virtualized List 就是優化長列表的一種技巧,名字聽起來很深奧,不過它的概念其實並不難理解:
儲存所有列表元素的位置,只渲染可視區 (viewport)內的列表元素,當可視區滾動時,根據滾動的 offset 大小以及所有列表元素的位置,計算在可視區應該渲染哪些元素,這種技術也叫做「Windowing」。
以上圖來說,假設可視區最多只能顯示 6 個 item,那即使我們的列表總共有 1000 個,也只會渲染出現在可視區的 6 個元素,當原本被渲染的 item 移出可視區後,就會被 unmount 掉,避免前面說的同時生成一堆 DOM 節點的狀況,也因此有效的解決了上面說的幾個缺點。
看看比較好理解的動圖
React 官方也建議要渲染長列表時使用 Windowing 技術
基本上現在的主流前端框架都有很完整的 Virtualized List 解決方案了
ReactJS: react-window, react-virtualized
VueJS: vue-virtual-scroll-list, vue-virtual-scroller
SvelteJS: svelte-virtual-list
在 React Native 的 App 開發世界裡,Virtualied List 甚至被做成了 built-in 的 component: VirtualizedList
因此,除非你有比較特殊需要客製化的需求,基本上不需要自己實作一個 Virtualized List。不過今天還是想來挑戰看看實作一個極簡型的 Virtualized List,畢竟親自寫 code,才能更快理解背後運作的原理,強調極簡型,是因為如果要實作一個完整且顧慮到各種情境的 Virtualized List 其實蠻複雜的(去看看上面各種套件的 source code 就知道了),所以今天只會實作最簡單最簡單的版本,目的是讓大家知道 Virtualized List 背後實作的原理。
因為筆者比較熟悉 React,所以會用 React 當作範例,不過會是一個非常簡易的版本,所以應該可以輕鬆地把同樣想法移植到其他 framework 上,主要目的還是理解 Virtualized List 的運作,就請對 React 不熟悉的讀者稍微忍耐一下了?
實作 Virtualized List 最關鍵的地方就是要計算當前哪些元素需要被渲染到畫面上,這些元素隨著使用者滑動會一直改變,為了快速實作一個簡易版的 Virtualized List,我決定採取一個比較簡單(ㄌㄢˇ ㄉㄨㄛˋ
)的方式,這個方式有幾個規則:
我們已經知道要丟到 Virtualized List 的資料的每一個 item 的 index,每一個 item 的高度也已經知道了,現在需要計算出要渲染在可視區的 item 的 index。在這之前我們有一些資訊要先取得:
讓我們先看看 SimpleVirtualizedList Component 的實作:
SimpleVirtualizedList Component 預設可以由上層傳入 4 個 properities:
根據這些上層傳入的 properties,我們可以取得剛剛說的其中兩個先備資訊:Inner Height(itemHeight * itemCount)與 Window Height。至於 Scroll Top 則會透過元件 container 的 scroll event 在使用者滾動視窗時動態去取得(也就是程式中的 onScroll function)。
得到先備資訊後再來需要計算的是在可視區內開始與結束的兩個 item 的 index,對應到程式碼的 startIndex 與 endIndex,這部分應該蠻好理解的。
到這裡,最基本且必要的資訊都取得了,因為 item 與可視區的高度都是寫死的,只要有 index 我們就可以計算出每一個 item 在整個列表(Long List)上的絕對位置,所以我選擇用偷懶的 position: absoulte 的方式來做排版。
實作到這裡,在使用者滾動視窗時,會去使用最新的 scrollTop 重新計算可視區內的 startIndex 與 endIndex,再根據新的 startIndex 與 endIndex 渲染新的 element 到可視區上。
再來看看要怎麼使用 SimpleVirtualizedList Component:
這邊為了方便 Demo,我直接建立了一個擁有 2000 個 items 的 dummy List 丟到 SimpleVirtualizedList 內,指定每個 item 的高度是 40px,可視區的高度是 400px,所以可視區內可以顯示的 item 數量為 10 個(400 / 40 = 10),renderItem 則是用來渲染 item 的 callback function。
讓我們來看看跑起來的結果
可以從 Browser Devtool 看到不管怎麼滑動,都只會渲染出在可視區範圍內的那 10 個 item elements。最基本款的 Virtualized List 終於可以 work 囉!
SimpleVirtualized 的 demo source code: https://github.com/kylemocode/it-ironman-2021/tree/master/simple-virtualized-list-demo
ResizeObserver API
動態決定可視區的 window height。雖然 Virtualized List 解決了一次渲染大量 DOM 元素的狀況,不過看完 demo 你有沒有發現一個很奇怪的地方,我們仍然需要將整個 List 的資料一次丟進 Virtualized List 裡面,當資料量一大,會對記憶體造成非常大的負擔,導致效能也變得低落。明天,我們來嘗試用 Lazy Loading 解決這個問題。
Virtualized List 如今已經成為前端應用不管是 Web 還是 App 都不可或缺的技術了,不管是逛電商是社群媒體,又或者是新聞平台,通常都會有長列表需要呈現,這時候 Virtualized List 就是優化應用效能上必備的技術了。今天除了介紹它的概念,也實作了一個最簡單最簡單的版本,讓讀者可以理解 Virtualized List 背後的實作原理。不過文章開頭也說過 Virtualized List 通常會搭配 Lazy Loading 的技術,才能發揮出最大的優化效果。至於什麼是 Lazy Loading,我們留到明天再說。各位明天見囉~
https://web.dev/virtualize-long-lists-react-window/
https://dev.to/nishanbajracharya/what-i-learned-from-building-my-own-virtualized-list-library-for-react-45ik
https://bvaughn.github.io/forward-js-2017/#/0/0