🪄

TanStack/Virtualの仮想スクロールをやってみた

2024/03/24に公開

こんにちは!@Ryo54388667です!☺️

普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

今回は仮想化ライブラリのTanStack/Virtualを触ったので紹介したいと思います。
https://tanstack.com/virtual/latest

📌 仮想スクロールについて

仮想スクロールとは、画面に表示されているデータの一部だけをロードして表示する技術です。
この"一部だけ"というのが重要です。

頻繁に利用されるシーンは、主に数万単位のリストデータが存在する時です。
百聞は一見にしかず、ということで下記の動画を見てください。ホバー時にリストのアイテムの背景を灰色にしていますが、この挙動がワンテンポ遅くなっています。

GIFなので少し分かりにくいですが、もっさりしています。。
リスト内の全てのデータがレンダリングされてしまっていることに起因しています。それを踏まえると、画面で見える部分だけ表示すれば効率的だ、という発想になりますね。これを実現する方法が画面に表示されているデータだけをロードして表示する、というものです。

もっと詳しく知りたいという方はこちらの記事がおすすめです!
図が非常にわかりやすいです✨勉強になりました!ありがとうございます🙏

https://qiita.com/neer_chan/items/5ff1a82ed2fe121026d5#スクロールした時の動き

これを自前実装するのは考慮事項が多いので骨が折れます。。
新規の部分をレンダリングしたり、逆に見えていた部分が隠れると、レンダリングしないようにしたりするなど、何かと大変です。ライブラリを利用するのが良いかと思います!

これまで、個人的に仮想スクロールといえばreact-windowというライブラリだったのですが、今回はtanstack-queryでお馴染みのtanstackから仮想化ライブラリが出ていたので、こちらを利用させてもらいました!比較検証の足しになれば幸いです。

📌 コードについて

Package

name version
react 18.2.0
@tanstack/react-virtual ^3.2.0

動的な行のリストの実装例

  const parentRef = useRef(null)

  const rowVirtualizer = useVirtualizer({
    count: list.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // アイテムの高さの推定値(カラムの場合は幅に相当する)
    overscan: 10,
  })

<ul className="block border-4 p-8 rounded-md" ref={parentRef} style={{
            height: 500,
            overflowY: 'auto',
            contain: 'strict',
          }}>
            <div style={{
              height: rowVirtualizer.getTotalSize(),
              width: '100%',
              position: 'relative',
            }}>
              <div
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                }}
              >
                {rowVirtualizer.getVirtualItems().map((virtualRow) => {
                  const photo = photos[virtualRow.index]
                  return (
        // ↓ リストの高さが固定の場合はrefの箇所をstyle属性でheightを固定します。
                    <li key={virtualRow.index} className="cursor-pointer hover:bg-slate-200 hover:rounded-md flex gap-4 items-center" ref={rowVirtualizer.measureElement}>
                      <div className="p-2 flex gap-4">
                        <input type="checkbox" className="cursor-pointer" />
                        <img src={photo.url} width={20} height={20} />
                      </div>
                      <p>{photo.title.length < 50 ? photo.title : photo.title.slice(0, 50) + "..."}</p>
                    </li>
                  )
                })}
              </div>
            </div>
          </ul>

公式参考
https://github.com/TanStack/virtual/blob/main/examples/react/dynamic/src/main.tsx

📌 仮想スクロールの効用

画面の挙動

シンプルな実装

仮想スクロールの実装

どちらも40000アイテムですが、ホバー時の挙動に差があります。
仮想スクロールの実装のほうは、即座に反映されています✨

消費メモリ

アプリはプロダクションモードでChromeのタブで見ていきます。

アイテム数 List Virtual
5000 275 60.5
20000 698 98.7
40000 835 142

※ 単位: MB

仮想スクロールの方は、レンダリングするアイテム数は変わらないので消費メモリは一定かなと思っていましたが、そうでもないらしいです。裏側でデータを保持しているので、そのぶんのメモリかなという予想です。

DOMContentLoadedの時間

表示スピードについては様々な指標がありますが、今回はこちらの指標を比較します。

アイテム数 List Virtual
5000 315 85
20000 603 251
40000 904 199

※ 単位: ms

リストが20000個と40000個のときはそれほど指標の値に変化がなかったのですが、5000個の時だけ少し早いような結果になりました。

📌 まとめ

そこまでインターフェースが複雑というわけではありません。
既存のリストに少し加える程度の変更なので導入しやすいのではないかと思います!
今回は仮想スクロールだけですが、tanstack/Virtualからは仮想グリッド(2次元)や仮想ウィンドウもあります。こちらも素振りしたいところです。

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667/status/1733434994016862256

Discussion