仮想スクロール

大量リストのうち ビューポートに見えている分だけを DOM に描く 描画最適化(windowing とも)。データ件数が増えても DOM ノード数を一定に保ち、メモリとフレームレートを安定させる。番組表グリッドのように数千行×時間軸を扱う UI では事実上必須。

基本メカニズム

  1. 全体サイズを確保 — 件数 × 行高(可変なら総和)でスペーサ高さを設定し、スクロールバーが全長を反映するようにする
  2. 可視範囲を計算 — スクロール位置から描画すべき index 範囲を出す
  3. その範囲だけ描画 — transform: translateY(offset) や絶対配置で正しい位置に置く
  4. スクロールに応じて要素が出入りし、フレームワークが差分描画(または DOM ノードを再利用)
// 固定行高の可視範囲(最も単純なケース、O(1))
const start = Math.floor(scrollTop / rowHeight)
const end   = Math.ceil((scrollTop + viewportH) / rowHeight)
const render = [start - overscan, end + overscan]  // overscan = 先読みバッファ

固定サイズ vs 可変サイズ

2次元への拡張

行・列の両方を仮想化すると巨大グリッドが扱える。EPG/番組表は 横=時間軸(番組長でセル幅可変)× 縦=チャンネル の典型例。横スクロール仮想化と可変幅セルが同時に効く。

オーバースキャン(先読みバッファ)

ビューポートの上下に 5〜10 行ほど多めに描画しておき、スクロールで滑り込む前に DOM へ存在させる。高速スクロール時の 白抜き・ポップインを防ぐ。広げすぎると描画コストが増えるトレードオフ。

落とし穴

ライブラリ(2026, React 中心)

ライブラリ形位置づけ
TanStack Virtualヘッドレス(useVirtualizer フック)最大の自由度。コンテナ/オフセット描画は自前。React/Vue/Svelte/Solid 横断
react-virtuosoコンポーネント実用の既定解。可変高・グループ・sticky ヘッダ・無限スクロールが組込み
react-windowコンポーネント軽量・枯れているが固定サイズ中心で開発停滞気味
Angular CDKcdk-virtual-scroll-viewportAngular 標準
vue-virtual-scrollerコンポーネントVue 定番

ヘッドレス(TanStack)= 計算だけ提供し DOM は自分で持つ。コンポーネント型(virtuoso/window)= DOM 構造をライブラリが握る代わりに記述量が少ない。

ネイティブ / Flutter — 仮想化は標準プリミティブに内蔵

最大の対比: Web は仮想化ライブラリを後付けするが、ネイティブ / Flutter は標準のリストプリミティブが最初から仮想化済み。やることは「正しいプリミティブを選ぶ」だけ。方式は2系統ある。

(a) ビュー再利用(view recycling) — 実際のビューオブジェクトをプールして使い回す

(b) 遅延コンポジション(lazy composition) — 可視分だけ生成し、画面外は破棄。古典的な「使い回し」ではなくランタイムが下層の描画オブジェクトを管理

プラットフォームプリミティブ方式先読みバッファ
WebTanStack / virtuoso 等(後付け)windowingoverscan(5〜10行)
iOS UIKitUITableView / UICollectionViewビュー再利用(dequeue)prefetching API
iOS SwiftUIList / LazyVStack遅延コンポジション自動
Android ViewRecyclerViewViewHolder 再利用LayoutManager の prefetch
Android ComposeLazyColumn / Row / Grid遅延コンポジション + スロット再利用自動
FlutterListView.builder / Sliver遅延 build + 要素再利用cacheExtent(既定 250px)

固定高の最適化は共通の発想: Flutter は itemExtent、UIKit は固定行高、Web は固定サイズで O(1)。レイアウト計測を省ける。

ネイティブ固有の落とし穴:

CSS だけの軽量代替 — content-visibility

content-visibility: auto + contain-intrinsic-size で、画面外要素のレンダリングをブラウザにスキップさせる(“受動的”仮想化)。実装が極小な一方、スクロールバードラッグ時のジャンク、アンカーリンクのスムーススクロール阻害などの癖がある。中規模なら有効、巨大データは本格的な仮想化が必要。

使わない判断

関連

Graph