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

超大型資料表和清單可能會大幅降低網站效能。虛擬化技術可以提供協助!

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 元件會接受 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 也支援將多維清單或格狀排列虛擬化。在這個情境中,當使用者水平垂直捲動時,可見內容的「視窗」會有所變動。

在虛擬化格狀中移動內容視窗是二維
在虛擬化格線中移動內容的「視窗」是二維的

同樣地,您可以視特定清單項目的大小是否可變動,決定是否使用 FixedSizeGridVariableSizeGrid 元件。

  • 對於 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:回呼會傳回承諾,該承諾會解析清單的其他資料

轉譯輔助物件用於傳回清單元件用於轉譯的函式。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 屬性,以免顯示空白內容。請勿過度掃描太多項目,否則會對效能造成負面影響。