🐎

大規模データのMaterial UIテーブルに仮想スクロールを導入して高速化する方法

2024/11/08に公開

仮想スクロールとは

UI上で今見えている部分だけをレンダリングして、スクロールするたびにレンダリング部分をシフトしていくことで、パフォーマンスを向上させる技術のことです。
見えていない部分はレンダリングしないので、大量のテーブルデータを扱ってもレンダリングが軽くなります。

なぜ仮想スクロールを導入するのか

例として、3000行のデータを持つテーブルを用意しました。
以下のように、通常のテーブルだと、再レンダリング時に3000行分のデータを一度にレンダリングするため、パフォーマンスが悪くなり、もっさりとした動きになってしまいます。
Image from Gyazo
仮想スクロール導入前

仮想スクロールを導入すると、同じ3000行のデータでも、画面に表示されている部分だけをレンダリングするため、スムーズに動作するようになります。
Image from Gyazo
仮想スクロール導入後

サンプルコードは、以下のCodeSandboxにあります。
https://codesandbox.io/p/sandbox/virtual-scroll-table-sknn89

無限スクロールとの違い

  • 無限スクロールは、スクロールするたびに新しいデータを読み込み続けてレンダリングするため、スクロールを続けると、レンダリング要素が増え続けてパフォーマンスが低下する可能性があります。
  • 仮想スクロールは、画面に表示されている範囲のみをレンダリングするため、スクロールを続けても、レンダリングされる要素数は一定なので、パフォーマンスが安定します。

Material UIのテーブルに仮想スクロールを導入する方法

Material UIの公式例で採用されていた、react-virtuosoを使用して実装します。
使い方は簡単で、以下のようにTableVirtuosoコンポーネントに行やカラムのコンポーネント、行データなどを渡すだけです。

<TableVirtuoso
  data={rows}
  components={VirtuosoTableComponents}
  fixedHeaderContent={Header}
  itemContent={(_index, row) => (
    <Row
      row={row}
      selectedRowIds={selectedRowIds}
      handleCheckboxChange={handleCheckboxChange}
    />
  )}
/>

実装の詳細は以下のファイルを参照してください。
https://codesandbox.io/p/sandbox/virtual-scroll-table-sknn89?file=%2Fsrc%2FVirtualScrollTable.tsx

詰まったポイント

1. 親要素で高さを指定しないとレンダリングされない

TableVirtuosoコンポーネントは、親要素の高さを取得して、その高さに合わせてレンダリングされるため、親要素に高さを指定しないと、レンダリングされずに真っ白になってしまいます。
以下のように、親要素で高さを指定しましょう。

VirtualScrollTable.tsx
<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の公式例の通りに実装すると、以下のように、再レンダリング時にスクロールバーが最上部に戻ってしまう現象が発生しました。
Image from Gyazo
スクロールバーが戻ってしまう現象

解決策としては、以下のようにScrollerのカスタムコンポーネントを削除し、TableVirtuosoコンポーネントがデフォルトのスクロールコンテナを使用するようにします。

VirtualScrollTable.tsx
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が参考になりました。
https://github.com/petyosi/react-virtuoso/issues/862

3. カラムの幅が可変になってしまう

仮想スクロールでは、UI上で見えている部分だけをレンダリングするため、カラムの幅が固定されていない場合、要素の長さによってカラムの幅が変わってしまいます。
Image from Gyazo
カラムの幅が可変になる現象

見栄えが悪いので、以下のようにカラムの幅を固定しておきましょう。

<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