透過回應視窗將大型清單虛擬化

大型表格和清單會使網站效能明顯降低。虛擬化就能派上用場!

react-window 程式庫可讓大型清單有效率地轉譯。

以下清單範例包含 1,000 個使用 react-window 轉譯的資料列。請盡可能加快捲動速度。

這種報表有哪些優點?

有時候,您需要顯示包含許多資料列的大型資料表或清單。載入這類清單上的每個項目可能會對效能產生重大影響。

清單虛擬化 (或稱「視窗化」) 的概念僅轉譯使用者可見的內容。最先算繪的元素數量是整份清單的一小部分,當使用者繼續捲動時,可見內容的「視窗」會移動。這可以改善清單的轉譯和捲動效能。

虛擬化清單中的內容視窗
移動虛擬化清單中的內容「視窗」

結束「視窗」的 DOM 節點會回收,或在使用者向下捲動清單時立即替換為較新的元素。這樣就能確保所有已轉譯元素數量都符合視窗大小。

回應視窗

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 元件接受 heightwidthitemSize 屬性,控制清單中項目的大小。
  • 用於轉譯資料列的函式會以子項的形式傳遞至 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 也支援虛擬化多維度清單或格線。在這種情況下,當使用者水平「和」垂直捲動時,可見內容的「視窗」也會隨之變更。

虛擬化格線中移動視窗的內容為 2D 效果
將內容的「視窗」移動至虛擬化格線中

同樣地,根據特定清單項目的大小是否有差異,可以使用 FixedSizeGridVariableSizeGrid 元件。

  • 如果是 FixedSizeGrid,API 大致相同,但資料欄和資料列都需要表示高度、寬度和項目數量。
  • 如果是 VariableSizeGrid,您可以將資料欄 (而非值) 傳入各自的屬性中,藉此變更資料欄寬度和列高度。

請參閱說明文件,查看虛擬化格線範例。

捲動時延遲載入

許多網站都會藉由等待載入並顯示長清單中的項目,直到使用者向下捲動,藉此提升效能。這項技巧通常稱為「無限載入」,會在使用者捲動經過接近末端的特定門檻時,將新的 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 內。指派給載入器的道具如下:

  • isItemLoaded:檢查特定項目是否已載入的方法
  • itemCount:清單 (或預期) 中的項目數量
  • loadMoreItems:傳回承諾會解析為清單的其他資料的回呼

系統會使用轉譯屬性傳回清單元件用於轉譯的函式。onItemsRenderedref 屬性都是需要傳入的屬性。

以下舉例說明無限載入如何與虛擬化清單搭配運作。

雖然向下捲動清單看起來也差不多,但現在系統會在您每次捲動至清單末端時,發出從隨機使用者 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 適用於 FixedSizeListVariableSizeList 元件,預設值為 1。根據清單的大小以及每個項目的大小,過度掃描多個項目,有助於防止使用者捲動畫面時,出現明顯空白空間的閃光。不過,過度掃描的項目可能會對效能造成負面影響。使用虛擬化清單的完整原則,是要盡量減少項目數量,讓使用者在任何指定時刻都能看到內容,因此請盡量減少過度掃描項目的數量。

如果是 FixedSizeGridVariableSizeGrid,請使用 overscanColumnsCountoverscanRowsCount 屬性分別控制要涵蓋的欄數與資料列數。

結論

如果不確定從何處開始虛擬應用程式中的清單和資料表,請按照下列步驟操作:

  1. 測量轉譯和捲動效能。這篇文章說明如何使用 Chrome 開發人員工具中的 FPS 計量器,探索項目轉譯清單的效率。
  2. 加入 react-window,找到影響效能的任何長清單或格線。
  3. 如果有 react-window 不支援的某些功能,請考慮使用 react-virtualized
  4. 如果您需要在使用者捲動畫面時延遲載入項目,請使用 react-window-infinite-loader 納入虛擬化清單。
  5. 為您的清單使用 overscanCount 屬性,並為格線使用 overscanColumnsCountoverscanRowsCount 屬性,以免空白內容閃爍。請勿過度掃描太多項目,以免對效能造成負面影響。