DOMは裏切る:数千件のリストを「正しく」捌く仮想スクロールの極意
Webアプリケーションが成熟するにつれ、私たちは「無限のデータ」を扱うという残酷な現実に直面します。APIから返ってきた5,000件のユーザーリストを、そのまま `map` 関数で `
数千のDOMノードがもたらすのは、初期レンダリングの遅延だけではありません。複雑なレイアウト計算(リフロー)と、CSSプロパティ変更のたびに発生する再描画(リペイント)が、ユーザーのスクロールをカクつかせ、UXを地の底まで沈めます。
今回は、単なる「ライブラリの導入」ではなく、ブラウザエンジンの挙動を理解した上で実装する、堅牢な「仮想スクロール(Windowing)」のアーキテクチャについて深掘りします。
—
1. 仮想スクロールの本質:DOMの「使い回し」
仮想スクロールの概念はシンプルです。「画面に見えている分だけをDOMとして生成し、残りは虚無とする」。しかし、その裏側には、スクロールイベントをいかに効率的に捌くかという泥臭い戦いがあります。
物理的なスクロールバーを持つコンテナの中に、見えない「スペーサー(総高さを確保する要素)」を置き、その上で現在表示すべき範囲の要素だけを絶対配置(Absolute Positioning)でレンダリングする。これが定石です。
アーキテクチャ上の注意点:メモリとリフロー
DOMノード数を一定に保つことは重要ですが、DOMの頻繁な「削除と生成」は、ガベージコレクション(GC)の負荷を増大させます。DOMを完全に破棄するのではなく、再利用可能なキャッシュプールを用意する、あるいはReactであれば `key` の管理を徹底し、最小限のノード差し替えに留める設計が必要です。
—
2. TypeScriptによる堅牢な実装設計
仮想スクロールを実装する際、最も厄介なのは「アイテムの高さが可変であるケース」です。固定長であれば計算は一瞬ですが、テキストの折り返しなどで高さが変動する場合、オフセットの計算は地獄と化します。
以下は、パフォーマンスと型安全を両立させるための最小構成モデルです。
/
- 仮想リストのアイテム情報を定義するインターフェース
/
interface VirtualItem {
index: number;
height: number;
top: number;
}
/
- 仮想スクロールのコンテナ設定
/
interface ViewportProps {
totalItems: number;
itemHeight: number; // 可変の場合は、各アイテムのオフセット値を保持する配列が必要
containerHeight: number;
}
// 効率的に表示すべき範囲を算出するロジック
const getVisibleRange = (scrollTop: number, props: ViewportProps) => {
const { itemHeight, containerHeight } = props;
// 最初のインデックスと最後のインデックスを算出
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
// バッファ(オーバーキャン)を加えてチラつきを防ぐ
const buffer = 5;
return {
start: Math.max(0, startIndex – buffer),
end: Math.min(props.totalItems – 1, endIndex + buffer),
};
};
—
3. 現場で陥る「エッジケース」という名の罠
理論上の実装が完成しても、現場では予期せぬバグが待ち構えています。
非同期データ取得と「スクロールジャンプ」
データが非同期に読み込まれ、リストの総数が変化すると、スクロール位置が突如としてズレる現象が発生します。これを防ぐには、リストの「先頭アイテム」を固定するか、スクロール位置をインデックスベースで維持する仕組みが不可欠です。
スクロールイベントの競合(Passive Event Listeners)
`scroll` イベントはブラウザのレンダリングサイクルを阻害しがちです。`{ passive: true }` オプションを使用し、イベントハンドラ内での `preventDefault()` を行わないことをブラウザに保証させることで、スムーズなスクロール体験を維持してください。
Intersection Observerの活用
最近では、自前で `scrollTop` を監視するのではなく、`IntersectionObserver` を活用して要素の出現を検知する手法も有効です。ただし、数千件のアイテムに対してObserverを大量に貼るのはオーバーヘッドが大きいため、「表示領域の端」にダミーの監視要素を置くテクニックが有効です。
—
4. 最後に:エンジニアとしての矜持
仮想スクロールの実装は、単なる機能追加ではありません。ブラウザという制約だらけの環境において、いかに計算量を減らし、メモリを節約し、ユーザーに「スムーズな体験」という幻想を見せ続けるかという、職人芸に近い領域です。
もしあなたが大規模データを扱うアプリを設計しているなら、まずは `react-window` や `tanstack-virtual` のソースコードを読んでみてください。彼らがどのような工夫でエッジケースを潰しているか、その「執念」が見えてくるはずです。
最高のパフォーマンスは、フレームワークの魔法ではなく、ブラウザの内部挙動を深く理解し、その上で泥臭く最適化を重ねた先にだけ存在します。あなたの書くコードが、今日もどこかのブラウザで軽快に動くことを願っています。

コメント