🕌

ダッシュボード表示のパフォーマンスを仮想スクロールを使って改善した話

2024/03/19に公開

はじめに

株式会社COUNTERWORKSでフロントエンドエンジニアをしている小木曽です。
普段はNext.jsを使用してwebアプリケーションを開発しております。

我々はSHOPCOUNTER Enterpriseという商業施設向けのリーシングDXシステムを開発しているのですが、最近ダッシュボード機能を開発しました。
ダッシュボード機能は、導入いただいている企業の事業上重要なデータを月ごとに集計した数値を確認することができる機能となっています。

ダッシュボードではデータを表で表示しており、我々が施設と呼称しているリソースが行、対象年月が列となっているのですが、
施設数、つまりダッシュボードの表における行数は数百から数千になることもあり、描画のパフォーマンスには配慮する必要がありました。

今回は仮想スクロールで高いパフォーマンスを実現することができたので、その手法について紹介します。

課題

これまでSHOPCOUNTER Enterpriseにおける、いわゆる無限スクロールの実装には、スクロールしていくごとにAPIをフェッチして描画を繰り返すといった一般的な方法を採用していました。
しかしながら、その方法ではスクロールするほど描画量が増えるため、パフォーマンスが低下するといった問題を抱えていました。

その問題に悩んでいたところ、仮想スクロールを知り導入してみることにしました。

仮想スクロールとは

仮想スクロールは画面に表示されている部分のみ読み込み、ユーザーがスクロールするとそれに応じて要素を動的に作成と削除をする技術です。
無限スクロールのようにスクロールする度にその分の要素を描画する必要がなくなり、ページ全体のメモリ使用量を一定に保つことができます。
↓のようにスクロールをしても<tr>数が一定なのがわかると思います。

※ちなみに、画面サイズを縮小すればブラウザに表示される要素は減るため、描画数はさらに減ります

実装方法

仮想スクロールの実装を行う際には、ライブラリを使用する方法としない方法があると思います。
ライブラリを使用しない場合、例えばIntersectionObserverを使用して要素を監視し、交差状態かを判定して要素を追加、削除する・・といった実装になると思います。
ただ、要素の高さが可変する場合には高さを取得してどの要素まで表示するかを計算したり、1番下の要素に到達したらイベントをトリガーしたりする処理をハードコーディングする必要があります。

ライブラリであれば、こういったことを自動でやってくれます。
ライブラリはいくつかありますが、今回はReact Virtuosoを使った実装例を載せたいと思います。

インストール

pnpm add react-virtuoso

実装

import { TableVirtuoso } from 'react-virtuoso';

const tableRows = [...] // テーブル行だとします

<TableVirtuoso
  totalCount={tableRows.length} // 項目数
  endReached={(index) => fetch(...)} // テーブル行の最後までスクロールしたら、呼ばれる(ここでフェッチなどを行う)
  fixedHeaderContent={() => { // テーブルのヘッダー内容
    return (
      <tr>
        <th className='py-4 px-2 text-center border-y border-solid border-gray-200 bg-white'>
          <div>項目</div>
        </th>
      </tr>
    );
  }}
  components={{ // テーブル要素をそれぞれ定義する
    Table: ({ style, ...props }) => {
      return (
        <table
          {...props}
            style={{
              ...style,
              tableLayout: 'fixed',
              width: '100%',
              borderCollapse: 'separate',
              borderSpacing: '0',
           }}
        />
      );
    },
    TableHead: ({ style, ...props }) => {
      return (
        <tbody {...props} />
      );
    },
    TableRow: (props) => {
      const index = props['data-index'];
      const row = tableRows[index];

      return (
        <tr {...props}>
          <td>{row.itemName}</td>
        </tr>
      );
    },
  }}
/>

上記はテーブル系のライブラリなどを使ってない場合の実装ですが、TanStackTableとも相性いいです。

import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { TableVirtuoso } from 'react-virtuoso';

// TanstackTableフック
const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();

// TableVirtuosoのTableRowでTanStackTableで定義したデータを利用する
<TableVirtuoso
  .
  .
  TableRow: (props) => {
    const index = props['data-index'];
    const row = rows[index];
    
    return (
      <tr {...props}>
        {row.getVisibleCells().map((cell) => {
          return (
            <td key={cell.id}>  
              {flexRender(cell.column.columnDef.cell, cell.getContext())}
            </td>
          );
        })}
      </tr>
    );
  },
/>

実装してみての感想

ダッシュボードテーブルでは描画だけでなく、他計算も含まれておりますが、仮想スクロールを使うことでメモリ使用量をかなり抑えることができました。
実装に関しては、React Virtuosoを使うことで低コストで実装できますし、他ライブラリとの相性もよく使いやすさという点でも良かったと感じています。

おわりに

ダッシュボードだけでなく、無限にスクロールして表示したいケースは多々あるかと思います。
こういうときに仮想スクロールが一つの選択肢としてあると良いと思います。
また今回はReact Virtuosoを利用して仮想スクロールを実現しましたが、TanStackTableにも仮想スクロール機能があります。
この記事が参考になると嬉しいです。

We are hiring!!

COUNTERWORKS では一緒に働く仲間を絶賛募集中です。
今後の更なる成長のためには圧倒的に仲間が不足しています。皆さまのご応募お待ちしております!

https://counterworks.co.jp/recruit/?utm_source=zenn&utm_medium=referral&utm_campaign=tech_blog&utm_content=2188a085c854c3

COUNTERWORKS テックブログ

Discussion