大規模データのMaterial UIテーブルに仮想スクロールを導入して高速化する方法
仮想スクロールとは
UI上で今見えている部分だけをレンダリングして、スクロールするたびにレンダリング部分をシフトしていくことで、パフォーマンスを向上させる技術のことです。
見えていない部分はレンダリングしないので、大量のテーブルデータを扱ってもレンダリングが軽くなります。
なぜ仮想スクロールを導入するのか
例として、3000行のデータを持つテーブルを用意しました。
以下のように、通常のテーブルだと、再レンダリング時に3000行分のデータを一度にレンダリングするため、パフォーマンスが悪くなり、もっさりとした動きになってしまいます。
仮想スクロール導入前
仮想スクロールを導入すると、同じ3000行のデータでも、画面に表示されている部分だけをレンダリングするため、スムーズに動作するようになります。
仮想スクロール導入後
サンプルコードは、以下のCodeSandboxにあります。
無限スクロールとの違い
- 無限スクロールは、スクロールするたびに新しいデータを読み込み続けてレンダリングするため、スクロールを続けると、レンダリング要素が増え続けてパフォーマンスが低下する可能性があります。
- 仮想スクロールは、画面に表示されている範囲のみをレンダリングするため、スクロールを続けても、レンダリングされる要素数は一定なので、パフォーマンスが安定します。
Material UIのテーブルに仮想スクロールを導入する方法
Material UIの公式例で採用されていた、react-virtuosoを使用して実装します。
使い方は簡単で、以下のようにTableVirtuoso
コンポーネントに行やカラムのコンポーネント、行データなどを渡すだけです。
<TableVirtuoso
data={rows}
components={VirtuosoTableComponents}
fixedHeaderContent={Header}
itemContent={(_index, row) => (
<Row
row={row}
selectedRowIds={selectedRowIds}
handleCheckboxChange={handleCheckboxChange}
/>
)}
/>
実装の詳細は以下のファイルを参照してください。
詰まったポイント
1. 親要素で高さを指定しないとレンダリングされない
TableVirtuoso
コンポーネントは、親要素の高さを取得して、その高さに合わせてレンダリングされるため、親要素に高さを指定しないと、レンダリングされずに真っ白になってしまいます。
以下のように、親要素で高さを指定しましょう。
<Box style={{ height: 300 }}> {/* ←親要素で高さを指定 */}
<TableVirtuoso
data={rows}
components={VirtuosoTableComponents}
fixedHeaderContent={Header}
itemContent={(_index, row) => (
<Row
row={row}
selectedRowIds={selectedRowIds}
handleCheckboxChange={handleCheckboxChange}
/>
)}
/>
</Box>
2. スクロールバーが戻ってしまう
Material UIの公式例の通りに実装すると、以下のように、再レンダリング時にスクロールバーが最上部に戻ってしまう現象が発生しました。
スクロールバーが戻ってしまう現象
解決策としては、以下のようにScroller
のカスタムコンポーネントを削除し、TableVirtuoso
コンポーネントがデフォルトのスクロールコンテナを使用するようにします。
const VirtuosoTableComponents: TableComponents<Data> = {
- Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
- <TableContainer component={Paper} {...props} ref={ref} />
- )),
Table: (props) => (
<Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />
),
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableHead {...props} ref={ref} />
)),
TableRow,
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableBody {...props} ref={ref} />
)),
};
以下のissueが参考になりました。
3. カラムの幅が可変になってしまう
仮想スクロールでは、UI上で見えている部分だけをレンダリングするため、カラムの幅が固定されていない場合、要素の長さによってカラムの幅が変わってしまいます。
カラムの幅が可変になる現象
見栄えが悪いので、以下のようにカラムの幅を固定しておきましょう。
<TableRow>
<TableCell />
<TableCell sx={{ width: 1 / 3 }}>ID</TableCell> {/* ←カラムの幅を固定 */}
<TableCell sx={{ width: 1 / 3 }}>Name</TableCell> {/* ←カラムの幅を固定 */}
<TableCell sx={{ width: 1 / 3 }}>Age</TableCell> {/* ←カラムの幅を固定 */}
</TableRow>
まとめ
- 仮想スクロールを導入することで、大量のテーブルデータを扱っても、パフォーマンスを維持して、スムーズに動作させることができました。
- ユースケースによっては、ページネーションや検索機能の導入で対処するのが望ましい場合もあると思いますので、適切な方法を選択しましょう。
Discussion