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

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

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 屬性,以免顯示空白內容。請勿過度掃描太多項目,以免對效能造成負面影響。