React-window | 有效地呈現大型列表與表格

你今天 windowing 了嗎?

Leo Chiu
手寫筆記
13 min readMar 21, 2020

--

前言

之前在學習 React 時翻了官方文件,在 Optimizing Performance 的章節中提到了 Virtualize Long List 這個概念。

如果在你的應用程式中渲染很長的列表,也許長度超過 100 行甚至 1000 行,這時你需要一種技術稱做「windowing」。這個技術讓應用程式只需要渲染長列表的某一部分,通常是使用者在瀏覽器上可見的地方,進而優化應用程式的效能。

官方先後 react-virtualizedreact-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 without windowing

你可以再試試看使用 react-window 後的差別,是不是變快很多。

但是,目前大多數網頁較少會一次載入大量的資料,都是選擇漸進式載入資料或是用分頁的方式減少前端的負載,非必要時,可能也不會第一時間就考量到使用 windowing 這個技術。

優化效能較低的設備瀏覽長列表的體驗

性能較低的設備在瀏覽長列表時,瀏覽器會花費大量的時間在 painting 上,很容易就會讓螢幕失幀,讓人感覺螢幕有點 Lag。

文章下半段有做一些有使用 react-window 與沒有使用在 Chrome 的測試數據,從數據中可以看到沒有使用 react-window 虛擬化長列表的網頁,它所消耗的 GPU Memory 比較多,因此,如果設備性能較低,可能會造成不好的使用這體驗。

沒有使用 react-window

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 打開方式如下:

  1. 在 Chrome 開啟 Developer Tool (F12)。
  2. ctrl + shift + p 開啟 Chrome 命令列。
  3. 輸入 rendering,選擇 [Drawer] Show Rendering。
  4. 在 Rendering 的 Tab 裡面,勾選 FPS meter 後就可以在瀏覽器中看到以上的介面。

在開啟後,我們不斷把網頁往下滾動,就可以看到類似於以下的數據,GPU Memory 大約使用了 20 MB。把 GPU Memory 的數字記錄下來,然後我們與使用了 React-window 的元件比較。(筆者的 GPU 是使用 GTX970)

non-windowing

同樣地,我們在新分頁打開有使用 react-window 的網頁,然後也像是上述一樣用 FPS meter 監控網頁在向下滾動的效能。

windowing

在沒有意外的情況下,我們會看到使用 react-window 的網頁 GPU Memory 比沒有 windowing 的網頁減少使用了 13 MB左右,幾乎少了 2 ~3 倍。

使用 Performance 監測瀏覽網頁的效能

我們同樣打開 F12 (Developer Tool),然後選擇「Performance」,接下來將用這個工具檢測兩個範例的差別。

我們將網頁向下滾動當作測試標的。

點擊左上角的「 ● 」,Chrome 就會開始記錄使用瀏覽器的效能。

Performance 檢測頁面

以下是測試大約 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 倍。

bundlephobia - 兩個套件的比教徒

作者在 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 之前可以先考量看看,是不是目前的網頁已經有效能上優化的需求,再選擇適合的套件優化它。

--

--

Leo Chiu
手寫筆記

每天進步一點點,在終點遇見更好的自己。 Instragram 小帳:@leo.web.dev