iT邦幫忙

2021 iThome 鐵人賽

DAY 10
4

Facebook、Instagram 應該都是我們日常生活中非常依賴的社群媒體了,每天閒來無事就要滑滑動態,看看朋友最近發生了什麼事。不過你有沒有注意到一件事,通常在滑動態的時候不會一次就把所有貼文載入完,而是會先載入一部分,等你滑動到底下後再接著載入更多貼文。這麼做的原因其實也是為了效能的優化,通常這樣的 feature 會由 Virtualized List 搭配 Lazy Loading 來完成,今天我將帶領各位認識 Virtualized List 的觀念,明天則會接著講解 Lazy Loading,廢話不多說,直接進入今天的正題吧!


什麼是 Virtualized List,為什麼需要它 ?

長列表 Long List,是網站中蠻常見的一個 feature,例如 FB 等社群媒體會有大量的文章列表,然而如果有 1000 篇文章,我們又將這些文章同時渲染的話,就必須生成 1000 個 DOM 節點,更不用說文章結構通常是相對複雜的,像這樣同時渲染數量龐大的元素會有幾個明顯的缺點:

  • 載入時白屏時間會比較長
  • 在渲染了大量 DOM 節點的狀況下,在滾動事件觸發時會大大增加記憶體的用量
  • 容易失幀,因為渲染很慢,所以無法維持瀏覽器的幀率,頁面會顯得卡頓
  • 最慘的話網頁會失去響應

而且這些問題在 Desktop 瀏覽器就會發生了,換作是手機瀏覽器只會讓問題變得更嚴重,因此這種狀況下我們應該優化長列表,提升使用者體驗。

Virtualized List 就是優化長列表的一種技巧,名字聽起來很深奧,不過它的概念其實並不難理解:

儲存所有列表元素的位置,只渲染可視區 (viewport)內的列表元素,當可視區滾動時,根據滾動的 offset 大小以及所有列表元素的位置,計算在可視區應該渲染哪些元素,這種技術也叫做「Windowing」。

以上圖來說,假設可視區最多只能顯示 6 個 item,那即使我們的列表總共有 1000 個,也只會渲染出現在可視區的 6 個元素,當原本被渲染的 item 移出可視區後,就會被 unmount 掉,避免前面說的同時生成一堆 DOM 節點的狀況,也因此有效的解決了上面說的幾個缺點。

看看比較好理解的動圖

React 官方也建議要渲染長列表時使用 Windowing 技術

Talk Is Cheap, Show Me The Code ! 實作一個極簡型的 Virtualized List 吧!

基本上現在的主流前端框架都有很完整的 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,我決定採取一個比較簡單(ㄌㄢˇ ㄉㄨㄛˋ
)的方式,這個方式有幾個規則:

  • 每個 List Item 的高度都一樣
  • 可視區視窗的高度是固定的
  • 因為每個 List Item 的高度都一樣,可視區視窗的高度也是固定的,只要知道 List 總共有幾個 item 再加上滑動距離就可以算出每個 item 相對於 Long List 的絕對距離,所以每個 item 的排版我選擇用簡單的 position: absolute 的方式來實作,這稍後會再詳細說明。

我們已經知道要丟到 Virtualized List 的資料的每一個 item 的 index,每一個 item 的高度也已經知道了,現在需要計算出要渲染在可視區的 item 的 index。在這之前我們有一些資訊要先取得:

  • Inner Height : 整個 Long List 的長度,因為已經設定每個 item 的高度是固定的,所以會是 Item Height * Item Number。
  • Window Height : Virtualized List 視窗的高度,也就是可視區的高度,在這個極簡版實作中,會由外部傳來寫死的值。
  • Scroll Top : 代表 Long List 被滑動的距離,也就是 Long List 的開頭(第一個 item)到可視區視窗內第一個 item 的距離。

讓我們先看看 SimpleVirtualizedList Component 的實作:

SimpleVirtualizedList Component 預設可以由上層傳入 4 個 properities:

  • itemHeight : 每個 List Item 的高度
  • itemCount : List 裡面總共的 item 數量
  • windowHeight : 可視區(滑動視窗)的高度
  • renderItem : 負責渲染在可視區內 items 的 callback function

根據這些上層傳入的 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

優化方向有哪些?

  • 支援每個 list item 的高度可以不一致,不過這樣會讓計算可視區要渲染哪些 item 比較複雜一點。
  • 從上方的影片 demo 可以看到滑動頁面同時重新渲染可視區元件時會讓畫面有一段時間是空白的,原因是這些元件是等已經進入可視區才進行渲染。要改善這個糟糕的 UX 可以透過預先渲染可視區邊界附近的小部分 element,當元素進入可視區時就可以讓使用者看到已經渲染完的結果。
  • 使用 ResizeObserver API 動態決定可視區的 window height。
  • 改成使用 position: absolute 搭配 transform(translateX, translateY) 來處理版面配置,利用 GPU 特性來得到較平順的動畫體驗。(這部分預計會在 Day 12 的時候介紹)

小反思,為明天做準備

雖然 Virtualized List 解決了一次渲染大量 DOM 元素的狀況,不過看完 demo 你有沒有發現一個很奇怪的地方,我們仍然需要將整個 List 的資料一次丟進 Virtualized List 裡面,當資料量一大,會對記憶體造成非常大的負擔,導致效能也變得低落。明天,我們來嘗試用 Lazy Loading 解決這個問題。

本日小結

Virtualized List 如今已經成為前端應用不管是 Web 還是 App 都不可或缺的技術了,不管是逛電商是社群媒體,又或者是新聞平台,通常都會有長列表需要呈現,這時候 Virtualized List 就是優化應用效能上必備的技術了。今天除了介紹它的概念,也實作了一個最簡單最簡單的版本,讓讀者可以理解 Virtualized List 背後的實作原理。不過文章開頭也說過 Virtualized List 通常會搭配 Lazy Loading 的技術,才能發揮出最大的優化效果。至於什麼是 Lazy Loading,我們留到明天再說。各位明天見囉~

References

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


上一篇
Day09 X Resource Hint & Non-Blocking Script Tag
下一篇
Day11 X Lazy Loading
系列文
今晚,我想來點 Web 前端效能優化大補帖!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言