前言
之前在學習 React 時翻了官方文件,在 Optimizing Performance 的章節中提到了 Virtualize Long List 這個概念。
如果在你的應用程式中渲染很長的列表,也許長度超過 100 行甚至 1000 行,這時你需要一種技術稱做「windowing」。這個技術讓應用程式只需要渲染長列表的某一部分,通常是使用者在瀏覽器上可見的地方,進而優化應用程式的效能。
官方先後 react-virtualized 與 react-window 這兩個套件,這兩個套件都提供了非常方便的元件,讓我們可以輕易的就實現 windowing。
最近找了時間看看這個議題在說些什麼,順便嘗試使用看看 react-window 的效果如何,並將學習的過程記錄下來與大家分享。
順帶一提
這兩個套件的作者是 React core team 的成員 Brian Vaughn,除了這兩個套件以外他還有負責 React Developer Tool,可以看看他在 React Conf 2019 的演講。
你需要使用 react-window 的原因
加速第一次頁面載入的時間
如果在網頁中需要渲染大量的 List、Table、Grid 等元件,在沒有任何優化的情況下,其中一個瓶頸是在第一次載入頁面時,必須等待所有的資料都已經載入到瀏覽器後,還要等待瀏覽器渲染與繪製網頁內容,所以,當資料量很大時會造成使用者等待的時間很久,讓使用者體驗 (UX) 非常不佳。
可以體驗以下沒有任何優化的範例,在範例中點擊「Load Data」的按鈕後,會生成 100 筆假資料並渲染到瀏覽器上。
你可以再試試看使用 react-window 後的差別,是不是變快很多。
但是,目前大多數網頁較少會一次載入大量的資料,都是選擇漸進式載入資料或是用分頁的方式減少前端的負載,非必要時,可能也不會第一時間就考量到使用 windowing 這個技術。
優化效能較低的設備瀏覽長列表的體驗
性能較低的設備在瀏覽長列表時,瀏覽器會花費大量的時間在 painting 上,很容易就會讓螢幕失幀,讓人感覺螢幕有點 Lag。
在文章下半段有做一些有使用 react-window 與沒有使用在 Chrome 的測試數據,從數據中可以看到沒有使用 react-window 虛擬化長列表的網頁,它所消耗的 GPU Memory 比較多,因此,如果設備性能較低,可能會造成不好的使用這體驗。
React-Window 實際運行的情況
我們點擊 codesandbox 視窗右上角的按鈕,瀏覽器會在新分頁打開 React 的應用程式,再打開 Developer Tool (F12) 看看滾動視窗時,DOM 會有怎樣的變化。
你會看到 React-window 會創建一個 position: absolute
的區塊,在這個區塊中的 <div />
都擁有固定的 top。在滾動時,最上方離開瀏覽器視窗的 DOM 都會被移除,並在下方插入新的 DOM。
與 React-window 的快樂小夥伴
Auto Sizer
在上面的範例中都有使用到這個元件。
AutoSizer
可以有效地自動管理 react-window 元件的空間,如果沒有這個套件,我們必須要自行計算元件可用的空間,才能讓元件的 height 與 width 與我們想的是一樣的。
在 react-virtualized 中提供了 AutoSizer
這個 HOC 元件,讓系統自動幫我們處理元件的空件。因此作者為了 react-window 將它獨立出來變成一個套件,在使用 react-window 時可以根據需求選擇載入這個好用的元件。
使用方法也非常的簡單,用 AutoSizer 將 react-window 元件包裹住,在把 AutoSizer 提供的 height 與 width 傳入到元件的 Props 中,如此一來,就可以讓它幫我們自動管理元件的 height 與 width。
Infinite Loader
除了基本的 List、Grid 以外,react-window 的作者還有因為 react-virtualized 做了 react-window-infinite-loader
,讓我們可以在 react-window 中輕易的實現無限捲動。
無限捲動:Facebook 大家一定很熟悉,他們為了增加使用者體驗,讓使用者不用一直點其他分頁,而是當使用者滾動到接近貼文底部後,系統會再自動載入新的貼文。
InfiniteLoader 元件基本有 3 個 Props:
isItemLoaded
(boolean):讓 InfiniteLoader 確認該行是否已經被載入。itemCount
(number):List 總共有幾行。loadMoreItems
(function):載入資料的函式。
將 InfiniteLoader 這個 HOC 元件包在 List 外面,當使用者滾動瀏覽器時,它會自動偵測目前的位置,並且透過 isItemLoaded
確認某列的資料已經被載入,然後在利用 loadMoreItems
載入新的資料。
React-window 在 Chrome 的效能分析
那麼,接下來我們來測試看看兩種做法在 Chrome 的效能如何。
使用 FPS Meter 監測 Frame Rate 與 GPU Memory
我們先測試沒有經過 react-window 優化的網頁,點擊 codesandbox 的範例介面右上角的按鈕,瀏覽器會在新分頁開啟網頁。然後我們用 Chrome 內建的功能 — FPS meter 監控網頁的效能,FPS meter 打開方式如下:
- 在 Chrome 開啟 Developer Tool (F12)。
ctrl + shift + p
開啟 Chrome 命令列。- 輸入 rendering,選擇 [Drawer] Show Rendering。
- 在 Rendering 的 Tab 裡面,勾選 FPS meter 後就可以在瀏覽器中看到以上的介面。
在開啟後,我們不斷把網頁往下滾動,就可以看到類似於以下的數據,GPU Memory 大約使用了 20 MB。把 GPU Memory 的數字記錄下來,然後我們與使用了 React-window 的元件比較。(筆者的 GPU 是使用 GTX970)
同樣地,我們在新分頁打開有使用 react-window 的網頁,然後也像是上述一樣用 FPS meter 監控網頁在向下滾動的效能。
在沒有意外的情況下,我們會看到使用 react-window 的網頁 GPU Memory 比沒有 windowing 的網頁減少使用了 13 MB左右,幾乎少了 2 ~3 倍。
使用 Performance 監測瀏覽網頁的效能
我們同樣打開 F12 (Developer Tool),然後選擇「Performance」,接下來將用這個工具檢測兩個範例的差別。
我們將網頁向下滾動當作測試標的。
點擊左上角的「 ● 」,Chrome 就會開始記錄使用瀏覽器的效能。
以下是測試大約 15 秒的數據,沒有使用 react-window 的網頁,在 painting 花費的時間是使用 react-window 的 10 倍以上。所以,如果在效能較低的設備上把一大堆的 DOM 全部一起放在網頁上,相會對設備造成很大的負擔。
順帶一提
至於 Scripting、Rendering、Painting、System 這些單詞在效能分析中只是一個概括的類別,從 Call Tree 中就可以看到光是 Scripting 就包含執行 JavaScript 事件、函式呼叫、GC 等。其他的單詞也包含了不少的事件,如果想要了解單詞的意思,可以看看 Google 官方的文件。
React-window vs React-virtualized
React-window 與 React-virtualized 都是同一個作者為 windowing 寫的套件,差別在於 React-window 效能更好、容量更小,但是實作的函式相對較少。
作者在 React-virtualized 的 README 有提到,在使用 React-virtualized 之前可以先看看 React-window 是否已經符合開發需求,如果符合可以考慮直接使用 React-window。
我們用 bundlephobia 看看兩個套件實際上的大小,大概差 5 倍左右,而且下載時間也幾乎差 5 倍。
作者在 react-window 的 README 也有提到兩者之間的差別,有興趣可以看看作者如何解釋。
API 比較
React-window 只有實作 List 跟 Grid 的元件,而 React-virtualized 還有實作Collection、Masonry、Table,甚至還有其他 8 個 HOC。相較之下 React-window 輕量非常多,如果需要額外的功能時再從其他套件引入。
React-window 的 Component:
FixedSizeList
VariableSizeList
FixedSizeGrid
VariableSizeGrid
React-virtualized 的 Component:
Collection
Grid
List
Masonry
Table
React-virtualized 的 High-Order Component:
ArrowKeyStepper
AutoSizer
CellMeasurer
ColumnSizer
InfiniteLoader
MultiGrid
ScrollSync
WindowScroller
如果想要看詳細的使用方法,可以瀏覽官方文件,官方文件的範例很豐富,而且都有附上 codesandbox,讓我們可以很容易了解套件怎麼使用。
結論
如果網頁需要呈現非常長的元件,在沒有任何優化的情況下,在第一次載入網頁時,使用者必須花費不少時間等待瀏覽器載入資料、繪製、渲染等流程。而且在效能較差的設備上,瀏覽器若必須處理大量的 DOM,將會讓設備造成較大的負擔,網頁可能會造成卡頓,進而讓使用者體驗變差。
所以,為了解決這個問題,有一種技術稱做 windowing,它會只呈長列表的一部分,並且伴隨使用者滾動瀏覽器時,會自動插入新的 DOM 或是刪除不必要的 DOM。
為此,React core team 的成員 Brian Vaughn 開發了 React-virtualized 這個工具,並在一段時間後重構了另一個 React-window 的套件,更快並且更小。作者建議如果 React-window 可以符合需求,便可以選擇不使用 React-virtualized。
在使用 React-window 或是 React-virtualized 之前可以先考量看看,是不是目前的網頁已經有效能上優化的需求,再選擇適合的套件優化它。
分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃
Reference
- Optimizing Performance
- react-window
- react-virtualized
- Windowing wars: React-virtualized vs. react-window
- Rendering large lists with React Virtualized
- Virtualize large lists with react-window
- Chrome Profiler: Self-Time vs. Total-Time
- Difference between ‘self’ and ‘total’ in Chrome CPU Profile of JS
- https://codesandbox.io/s/functional-infinite-list-with-virtualized-wdwn6