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

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

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

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

なぜこれが有用なのでしょうか。

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

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

仮想化されたリスト内のコンテンツのウィンドウ
仮想化されたリスト内のコンテンツの「ウィンドウ」を移動する

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

react-window

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 次元です

同様に、特定のリストアイテムのサイズが異なるかどうかに応じて、FixedSizeGrid コンポーネントと VariableSizeGrid コンポーネントの両方を使用できます。

  • 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: リストの追加データに解決するプロミスを返すコールバック

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

以下は、仮想化されたリストで無限読み込みを機能させる方法の例です。

リストを下にスクロールしても同じに見えますが、リストの最後近くまでスクロールするたびに、random user 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>

overscanCountFixedSizeList コンポーネントと VariableSizeList コンポーネントの両方で機能し、デフォルト値は 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 プロパティを使用します。エントリを過度にオーバースキャンすると、パフォーマンスに悪影響を及ぼすため、注意してください。