超大型資料表和清單可能會大幅降低網站效能。虛擬化技術可以提供協助!
react-window
是一個程式庫,可讓您有效率地算繪大型清單。
以下是使用 react-window
算繪的清單範例,其中包含 1000 列。請盡可能快速捲動畫面。
這種報表有哪些優點?
有時您可能需要顯示包含多個資料列的大型表格或清單。載入這類清單上的每個項目,可能會對效能造成重大影響。
清單虛擬化或「視窗化」是指只算繪使用者可見的內容。一開始算繪的元素數量是整個清單中的一小部分,且當使用者繼續捲動時,可見內容的「視窗」會移動。這麼做可改善清單的算繪和捲動效能。
離開「視窗」的 DOM 節點會回收,或是在使用者向下捲動清單時立即替換成較新的元素。這麼做可保留所有轉譯元素的數量,並依視窗大小而定。
react-window
react-window
是小型第三方程式庫,可讓您在應用程式中輕鬆建立虛擬化清單。它提供多個可用於不同類型清單和資料表的基本 API。
使用固定大小清單的時機
如果您有長的單維清單,且清單中項目的大小相同,請使用 FixedSizeList
元件。
import React from 'react';
import { FixedSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const ListComponent = () => (
<FixedSizeList
height={500}
width={500}
itemSize={120}
itemCount={items.length}
>
{Row}
</FixedSizeList>
);
export default ListComponent;
FixedSizeList
元件會接受height
、width
和itemSize
屬性,用於控制清單中項目的大小。- 會轉譯資料列的函式會以子項的形式傳遞至
FixedSizeList
。您可以使用index
引數 (items[index]
) 存取特定項目的詳細資料。 style
參數也會傳入資料列轉譯方法,該方法必須附加至資料列元素。清單項目會以絕對位置顯示,並在內文中指定高度和寬度值,而style
參數負責執行這項操作。
本文稍早顯示的 Glitch 範例,就是 FixedSizeList
元件的範例。
使用可變長度清單的時機
使用 VariableSizeList
元件轉譯不同大小的項目清單。這個元件的運作方式與固定大小清單相同,但會預期 itemSize
屬性的函式,而非特定值。
import React from 'react';
import { VariableSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const getItemSize = index => {
// return a size for items[index]
}
const ListComponent = () => (
<VariableSizeList
height={500}
width={500}
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
export default ListComponent;
以下嵌入內容為此元件的範例。
在這個範例中,傳遞至 itemSize
屬性的項目大小函式會隨機產生列高度。不過,在實際應用程式中,應有實際的邏輯定義每個項目的大小。理想情況下,這些大小應根據資料計算,或從 API 取得。
格線
react-window
也支援將多維清單或格狀排列虛擬化。在這個情境中,當使用者水平和垂直捲動時,可見內容的「視窗」會有所變動。
同樣地,您可以視特定清單項目的大小是否可變動,決定是否使用 FixedSizeGrid
和 VariableSizeGrid
元件。
- 對於
FixedSizeGrid
,API 大致相同,但事實上,需要為欄和列同時呈現高度、寬度和項目計數。 - 針對
VariableSizeGrid
,您可以將函式 (而非值) 傳遞至各自的 props,藉此變更欄寬和列高。
請參閱說明文件,查看虛擬化格線的範例。
捲動時延遲載入
許多網站會等到使用者向下捲動時,才載入並轉譯長清單中的項目,藉此提升效能。這種技巧通常稱為「無限載入」,會在使用者捲動至接近結尾的特定門檻時,將新的 DOM 節點新增至清單中。雖然這比一次載入清單中的所有項目要好,但如果使用者捲動超過數千個項目,這麼做最終還是會在 DOM 中填入數千個資料列項目。這可能導致 DOM 大小過大,進而影響效能,因為樣式計算和 DOM 變異會變慢。
下圖可幫助您瞭解這項功能:
解決這個問題的最佳方法,就是繼續使用 react-window
等程式庫,在頁面上維持元素的小型「視窗」,但在使用者向下捲動時,也要延後載入較新的項目。react-window-infinite-loader
是另一個套件,可透過 react-window
實現這項功能。
請參考以下程式碼片段,其中顯示在父項 App
元件中管理的狀態範例。
import React, { Component } from 'react';
import ListComponent from './ListComponent';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: [], // instantiate initial list here
moreItemsLoading: false,
hasNextPage: true
};
this.loadMore = this.loadMore.bind(this);
}
loadMore() {
// method to fetch newer entries for the list
}
render() {
const { items, moreItemsLoading, hasNextPage } = this.state;
return (
<ListComponent
items={items}
moreItemsLoading={moreItemsLoading}
loadMore={this.loadMore}
hasNextPage={hasNextPage}
/>
);
}
}
export default App;
loadMore
方法會傳遞至包含無限載入器清單的子項 ListComponent
。這項指標非常重要,因為無限載入器需要在使用者捲動到某個點時,觸發回呼來載入更多項目。
以下是算繪清單的 ListComponent
外觀:
import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";
const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
const Row = ({ index, style }) => (
{/* define the row component using items[index] */}
);
const itemCount = hasNextPage ? items.length + 1 : items.length;
return (
<InfiniteLoader
isItemLoaded={index => index < items.length}
itemCount={itemCount}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
width={500}
itemCount={itemCount}
itemSize={120}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
)
};
export default ListComponent;
在此,FixedSizeList
元件會包裝在 InfiniteLoader
中。指派給載入器的 props 如下:
isItemLoaded
:檢查特定項目是否已載入的方法itemCount
:清單中的項目數量 (或預期數量)loadMoreItems
:回呼會傳回承諾,該承諾會解析清單的其他資料
轉譯輔助物件用於傳回清單元件用於轉譯的函式。onItemsRendered
和 ref
屬性都是需要傳入的屬性。
以下範例說明無限載入功能如何與虛擬化清單搭配運作。
向下捲動清單的體驗可能會相同,但現在每次捲動至清單結尾時,系統都會發出要求,從隨機使用者 API 擷取 10 位使用者。這一切都是在一次只算繪製單一「視窗」的結果時完成。
檢查特定項目的 index
後,可根據是否已提出新項目要求,以及項目是否仍在載入,為項目顯示不同的載入狀態。
例如:
const Row = ({ index, style }) => {
const itemLoading = index === items.length;
if (itemLoading) {
// return loading state
} else {
// return item
}
};
過掃
由於虛擬化清單中的項目只會在使用者捲動時變更,因此當新項目即將顯示時,空白空間可能會短暫閃爍。您可以嘗試快速捲動本指南先前的任何範例,看看是否有這項現象。
為改善虛擬化清單的使用者體驗,react-window
允許您使用 overscanCount
屬性對項目進行過度掃描。這樣一來,您就能定義在可見「視窗」外要算繪的項目數量。
<FixedSizeList
//...
overscanCount={4}
>
{...}
</FixedSizeList>
overscanCount
適用於 FixedSizeList
和 VariableSizeList
元件,預設值為 1。視清單大小和每個項目的大小而定,如果掃描的項目數量超過一個,就能避免使用者捲動時出現空白空間的閃爍效果。不過,如果掃描項目過多,可能會對效能造成負面影響。使用虛擬化清單的目的,是盡可能減少使用者在任何時間點可見的項目數量,因此請盡量減少過度掃描的項目數量。
針對 FixedSizeGrid
和 VariableSizeGrid
,請使用 overscanColumnsCount
和 overscanRowsCount
屬性,分別控制要進行過掃的欄和列數量。
結論
如果不確定應用程式中的清單和資料表要從何處開始虛擬化,請按照下列步驟操作:
- 評估轉譯和捲動效能。這篇文章將說明如何使用 Chrome 開發人員工具中的 FPS 計量器,探索項目在清單上顯示的效率。
- 針對影響效能的長清單或格線,加入
react-window
。 - 如果
react-window
不支援某些功能,且您無法自行新增此功能,請考慮使用react-virtualized
。 - 如果您需要在使用者捲動時延後載入項目,請使用
react-window-infinite-loader
包裝虛擬化清單。 - 請為清單使用
overscanCount
屬性,並為格線使用overscanColumnsCount
和overscanRowsCount
屬性,以免顯示空白內容。請勿過度掃描太多項目,否則會對效能造成負面影響。