リアクション ウィンドウを使用して大きなリストを仮想化する

テーブルやリストが非常に大きいと、サイトのパフォーマンスが大幅に低下する可能性があります。仮想化が役立ちます。

react-window は、大規模なリストを効率的にレンダリングできるようにするライブラリです。

以下に、react-window でレンダリングされる 1,000 行を含むリストの例を示します。できるだけ速くスクロールしてみてください。

メリット

場合によっては、多数の行を含む大きなテーブルやリストを表示しなければならないことがあります。このようなリストのすべてのアイテムを読み込むと、パフォーマンスに大きな影響を与える可能性があります。

リストの仮想化、つまり「ウィンドウ処理」は、ユーザーに表示されるもののみをレンダリングするという概念です。最初にレンダリングされる要素の数はリスト全体のごく一部であり、ユーザーがスクロールを続けると、表示されるコンテンツの「ウィンドウ」が移動します。これにより、リストのレンダリングとスクロールの両方のパフォーマンスが向上します。

仮想化リストのコンテンツ ウィンドウ
仮想化リスト内でのコンテンツの「ウィンドウ」の移動

「ウィンドウ」から出る DOM ノードはリサイクルされるか、ユーザーがリストを下にスクロールするとすぐに新しい要素に置き換えられます。これにより、レンダリングされるすべての要素の数がウィンドウのサイズに固有のものになります。

反応ウィンドウ

react-window は、アプリで仮想化されたリストを簡単に作成できる小さなサードパーティ ライブラリです。さまざまな種類のリストやテーブルに使用できるさまざまな基本 API が用意されています。

固定サイズのリストを使用するタイミング

サイズが等しいアイテムからなる長い 1 次元リストがある場合は、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 は、多次元リスト(グリッド)の仮想化もサポートします。このコンテキストでは、ユーザーが水平方向および垂直方向にスクロールすると、表示されるコンテンツの「ウィンドウ」が変化します。

仮想グリッドでコンテンツを表示するウィンドウを 2 次元で移動する
仮想グリッド内でコンテンツの「ウィンドウ」を 2 次元で移動する

同様に、特定のリストアイテムのサイズが変わるかどうかに応じて、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: リストの追加データに解決される Promise を返すコールバック

レンダリング プロパティは、リスト コンポーネントがレンダリングに使用する関数を返すために使用されます。onItemsRendered 属性と ref 属性は、いずれも渡す必要がある属性です。

仮想化リストでの無限読み込みの仕組みの例を次に示します。

リストを下にスクロールしても同じように感じるかもしれませんが、リストの最後にスクロールするたびに、ランダム ユーザー API から 10 人のユーザーを取得するリクエストが行われます。これはすべて、一度に 1 つの結果の「ウィンドウ」のみをレンダリングしながら行われます。

特定のアイテムの 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>

overscanCountFixedSizeListVariableSizeList の両方のコンポーネントで機能し、デフォルト値は 1 です。リストのサイズと各アイテムのサイズによっては、複数のエントリをオーバースキャンすることで、ユーザーがスクロールしたときに、目立った空白スペースがフラッシュするのを防ぐことができます。ただし、エントリを過剰にスキャンすると、パフォーマンスに悪影響を及ぼす可能性があります。仮想化リストを使用する目的は、ある時点でユーザーに表示されるエントリ数を最小限に抑えることです。そのため、オーバースキャンされるアイテムの数はできるだけ少なくしてください。

FixedSizeGridVariableSizeGrid では、overscanColumnsCount プロパティと overscanRowsCount プロパティを使用して、オーバースキャンする列と行の数をそれぞれ制御します。

おわりに

アプリケーション内でリストとテーブルの仮想化をどこから開始すればよいかわからない場合は、次の手順を行います。

  1. レンダリングとスクロールのパフォーマンスを測定します。この記事では、Chrome DevTools の FPS メーターを使用して、リストのアイテムがどの程度効率的にレンダリングされるかを調べる方法について説明します。
  2. パフォーマンスに影響している長いリストまたはグリッドには、react-window を含めます。
  3. react-window でサポートされていない特定の機能がある場合、この機能を自分で追加できない場合は、react-virtualized の使用を検討してください。
  4. ユーザーのスクロールに合わせてアイテムを遅延読み込みする必要がある場合は、仮想化リストを react-window-infinite-loader でラップします。
  5. 空のコンテンツのフラッシュを防ぐには、リストに overscanCount プロパティを使用し、グリッドには overscanColumnsCount プロパティと overscanRowsCount プロパティを使用します。パフォーマンスに悪影響を及ぼすため、エントリを過度にオーバースキャンしないでください。