はじめに
チャットアプリのメッセージ一覧、ECサイトの商品一覧、ログビューアーのレコード表示。件数が数百、数千と増えていくと、決まって画面がカクつき始めます。原因はシンプルで、DOMに要素を積み上げすぎているからです。
「表示されていない要素までブラウザに描画させているのが悪い」とわかっていても、自分で仮想スクロールを実装するのは意外と骨が折れます。可変サイズの要素、スクロール位置の復元、iOSでの挙動差異……。こうした面倒事を丸ごと引き受けてくれるのが、今回紹介するVirtuaです。
Virtuaとは
Virtuaは、React・Vue・Solid・Svelteに対応した仮想スクロール(バーチャルリスト・グリッド)コンポーネントです。「ゼロコンフィグで高速・軽量(約3kB)」を掲げており、要素の高さを指定しなくても自動で計測しながら描画してくれます。
主な特徴
- ゼロコンフィグ - 各アイテムの高さや幅を事前に指定しなくても、実際のサイズを自動計測しながら仮想化してくれます
- 軽量 - gzip圧縮後で約3kBと非常に小さく、バンドルサイズへの影響を最小限に抑えられます
- マルチフレームワーク対応 - React・Vue・Solid・Svelteそれぞれ向けのパッケージが用意されており、同じ思想のAPIで使えます
- 実運用向けの作り込み - 水平/垂直スクロール、RTL対応、ウィンドウ全体をスクロールコンテナにする
WindowVirtualizer、無限スクロール、スクロール位置の復元など、実際のプロダクトで必要になる機能が一通り揃っています
インストール
npm、yarn、pnpmのいずれからでもインストールできます。
# npm
npm install virtua
# yarn
yarn add virtua
# pnpm
pnpm add virtua
基本的な使い方
最もシンプルな例として、Reactで1000件のリストを仮想スクロールさせてみます。
import { VList } from "virtua";
function App() {
return (
<VList style={{ height: 600 }}>
{Array.from({ length: 1000 }).map((_, i) => (
<div key={i} style={{ padding: 8, borderBottom: "1px solid #eee" }}>
Row {i}
</div>
))}
</VList>
);
}
高さを指定していない子要素でも、Virtuaが実際のサイズを自動計測してスクロール位置を計算してくれます。可変サイズの要素が混在するリストでも、特別な設定は不要です。
Vueの場合はコンポーネント名がVListのまま、テンプレート構文で利用できます。
<script setup>
import { VList } from "virtua/vue";
const items = Array.from({ length: 1000 }, (_, i) => i);
</script>
<template>
<VList :data="items" #default="{ item }" style="height: 600px">
<div class="row">Row {{ item }}</div>
</VList>
</template>
実践的なユースケース
無限スクロールのチャットログ
チャットアプリのように、上にスクロールすると過去ログを追加取得したいケースでは、VListのonScrollと組み合わせて実装します。
import { useRef, useState, useCallback } from "react";
import { VList, VListHandle } from "virtua";
type Message = { id: number; text: string };
function ChatLog({ initialMessages }: { initialMessages: Message[] }) {
const [messages, setMessages] = useState(initialMessages);
const [loading, setLoading] = useState(false);
const listRef = useRef<VListHandle>(null);
const loadMore = useCallback(async () => {
if (loading) return;
setLoading(true);
const older = await fetchOlderMessages(messages[0]?.id);
setMessages((prev) => [...older, ...prev]);
setLoading(false);
}, [loading, messages]);
return (
<VList
ref={listRef}
style={{ height: 500 }}
onScroll={(offset) => {
if (offset < 100 && !loading) {
loadMore();
}
}}
>
{messages.map((m) => (
<div key={m.id} style={{ padding: 8 }}>
{m.text}
</div>
))}
</VList>
);
}
// APIから過去のメッセージを取得する想定の関数
async function fetchOlderMessages(beforeId?: number): Promise<Message[]> {
const res = await fetch(`/api/messages?before=${beforeId ?? ""}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
return res.json();
}
上方向にスクロールして古いメッセージを先頭に追加しても、Virtuaはスクロール位置がガクッとズレないよう自動で補正してくれます。これは自前実装だと地味に苦労するポイントなので、非常にありがたい機能です。
ページ全体をスクロールさせたい場合
一覧をカード型グリッドの中の一部として使いたい、つまりページ全体のスクロールに追従させたい場合はWindowVirtualizerを使います。
import { WindowVirtualizer } from "virtua";
function ArticleList({ articles }: { articles: { id: number; title: string }[] }) {
return (
<WindowVirtualizer count={articles.length}>
{(i) => (
<article key={articles[i].id} style={{ padding: 16 }}>
<h2>{articles[i].title}</h2>
</article>
)}
</WindowVirtualizer>
);
}
VListが独自のスクロールコンテナを持つのに対し、WindowVirtualizerはブラウザのウィンドウそのものをスクロール領域として扱います。ブログ記事一覧やLPのような、ページ全体がスクロールするレイアウトに向いています。
まとめ
Virtuaは、仮想スクロールという「実装は理解できても、いざ自分で書くと沼にハマりやすい」領域を、ゼロコンフィグかつ軽量に解決してくれるライブラリです。
- 可変サイズの要素でも自動計測してくれる
- React・Vue・Solid・Svelteとフレームワークを問わず同じ思想で使える
- 無限スクロールやウィンドウスクロールなど実運用に必要な機能が揃っている
大量データを扱うリストやテーブルでパフォーマンスに悩んでいるなら、まずはVListを差し込むところから試してみてはいかがでしょうか。体感できるほどスクロールが軽くなるはずです。
