非常に大きなテーブルやリストは、サイトのパフォーマンスを大幅に低下させる可能性があります。仮想化が役立ちます。
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
コンポーネントは、リスト内のアイテムのサイズを制御するheight
、width
、itemSize
プロパティを受け入れます。- 行をレンダリングする関数は、子として
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
は、多次元リストやグリッドの仮想化もサポートしています。このコンテキストでは、ユーザーが水平方向と垂直方向にスクロールすると、表示されるコンテンツの「ウィンドウ」が変化します。

同様に、特定のリストアイテムのサイズが変化するかどうかによって、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
: リストの追加データを解決する 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>
overscanCount
は FixedSizeList
コンポーネントと VariableSizeList
コンポーネントの両方で機能し、デフォルト値は 1 です。リストの大きさや各アイテムのサイズに応じて、1 つのエントリだけでなく、より多くをオーバースキャンすることで、ユーザーがスクロールしたときに空白が目立つように表示されるのを防ぐことができます。ただし、エントリを過度にスキャンすると、パフォーマンスに悪影響を及ぼす可能性があります。仮想化されたリストを使用する目的は、ユーザーが任意の時点で確認できるエントリの数を最小限に抑えることなので、オーバースキャンされたアイテムの数をできるだけ少なくするようにしてください。
FixedSizeGrid
と VariableSizeGrid
の場合、overscanColumnsCount
プロパティと overscanRowsCount
プロパティを使用して、それぞれオーバースキャンする列と行の数を制御します。
まとめ
アプリケーションでリストとテーブルの仮想化をどこから始めればよいかわからない場合は、次の手順に沿って操作します。
- レンダリングとスクロールのパフォーマンスを測定します。この記事では、Chrome DevTools の FPS メーターを使用して、リスト上のアイテムがどれだけ効率的にレンダリングされているかを確認する方法について説明します。
- パフォーマンスに影響する長いリストやグリッドには
react-window
を含めます。 react-window
でサポートされていない機能がある場合は、その機能を自分で追加できない場合にreact-virtualized
の使用を検討してください。- ユーザーがスクロールしたときにアイテムを遅延読み込みする必要がある場合は、仮想化されたリストを
react-window-infinite-loader
でラップします。 - リストには
overscanCount
プロパティ、グリッドにはoverscanColumnsCount
プロパティとoverscanRowsCount
プロパティを使用して、空のコンテンツがちらつくのを防ぎます。エントリを過度にスキャンしないでください。パフォーマンスに悪影響を及ぼします。