誰も語らないDOMのボトルネック
サポートダッシュボードが1万件のログエントリを読み込んでいる場面を想像してみてください。あるいは、CRMがすべての連絡先を単一のスクロール可能なテーブルに表示しようとしている場面を。Reactでこれを構築するコードは、一見すると全く問題なさそうに見えます。配列をmapし、JSXを返し、フレームワークに任せるだけです。開発環境で100行程度なら、すべてが問題なく動作します。しかし、本番データが投入されると、ページは泥のように重くなります。
ブラウザが怠慢なわけではありません。ブラウザは指示通りに動いているだけなのです。そして、それこそが問題なのです。すべての行がDOMノードになります。各ノードにはスタイルが適用され、レイアウトが計算され、描画(ペイント)され、メモリ内で追跡されます。スクロールすると、ブラウザは現在見ている部分だけでなく、ツリー全体の位置を再計算します。イベントリスナーが積み重なり、メモリ使用量が急増します。最終的にメインスレッドが窒息し、インターフェースがクリックやキー入力、あるいはスクロールにさえ反応しなくなります。技術的な意味でアプリケーションがクラッシュしたわけではありませんが、目の前にいるユーザーにとっては、体験が損なわれていることに変わりはありません。
これは、ブラウザがあらゆる要素を一度にアクティブなメモリ上に保持しようとするために起こります。ReactはUIの仮想的な記述を作成することには長けているかもしれませんが、それらの記述がドキュメント内の実際のノードになると、手書きのHTMLと同じコストがかかるようになります。フレームワーク自体に逃げ道はありません。リストをDOMに渡す方法を構造的に変える必要があります。
仮想化(Virtualization)の真の意味
仮想化とは、その構造的な変更のことです。Reactに配列全体をレンダリングさせる代わりに、ビューポート内に収まるアイテムと、その上下にあるわずかなバッファのみをレンダリングします。ユーザーがスクロールすると、アプリケーションは画面外に移動したノードを破棄し、反対側から入ってくる新しいノードをインスタンス化します。総スクロール高さが(通常は単一の背の高いコンテナ要素や、慎重に計算されたスペーサーによって)維持されているため、ユーザーには依然として一つの連続したリストのように感じられます。表示されているアイテムは、データセットの上をスライドする「窓」に過ぎません。
プロジェクターのゲートを通過するフィルムストリップを想像してみてください。観客は滑らかな動きを見ますが、装置は現在位置にあるフレームだけを照らします。リールの残りは供給側と巻き取り側のスプールに存在しており、光の経路にはありません。仮想化リストもこれと同じ仕組みです。データセットがリールであり、ビューポートがゲートなのです。
これは伝統的な意味での「遅延読み込み(Lazy loading)」ではありません。遅延読み込みは、ユーザーがその近くまでスクロールするまでデータの取得を遅らせるものです。仮想化は、すでにデータを持っていることを前提とし、どの部分を実際のDOM要素に昇格させるかを厳選します。これら2つの手法は併用できますが、解決する問題は異なります。
なぜその違いが即座に感じられるのか
メリットは4つの側面で現れますが、それらはすべて「ユーザーが見ることのできないものに対してコストを支払わなくなる」という共通の根本的な解決策に結びついています。
初期ロード時間の短縮。 ブラウザがページを開くとき、1万5千行ではなく、おそらく15行程度を描画します。最初の意味のある描画(First Meaningful Paint)がより早く到達します。JavaScriptエンジンがノードの作成とドキュメントへのアタッチに費やす時間が減るため、Time-to-Interactive(操作可能になるまでの時間)も低下します。
メモリ使用量の削減。 DOMノードはコストの高いオブジェクトです。各ノードは、スタイルルール、レイアウトメトリクス、イベントバインディングへの参照を保持しています。アクティブなノード数を数十個に絞り込めば、メモリフットプリントは劇的に減少します。低スペックのデバイスや長時間のセッションでは、これだけでOSによるタブの強制終了を防ぐことができます。
スムーズなスクロール性能。 ツリー内のノードが少なくなると、スクロールイベント中のレイアウトおよびペイントフェーズに費やす時間が短縮されます。コンポジタースレッドは、隠れたコンテンツのジオメトリを常に再計算することなく、動きを処理できます。その結果、モニターのリフレッシュレートに近いスムーズなスクロールが実現します。
安定したフレームレート。 メインスレッドがレイアウト作業に溺れることがなくなるため、他のアクティビティのための余力が生まれます。アニメーションは滑らかなまま保たれ、ネットワークのレスポンスも処理できます。レンダリングパスがボトルネックではなくなるため、新しいデータが到着してもUIがフリーズすることはありません。
実装を正しく行うために
Reactのエコシステムでは、react-windowや、より多機能なreact-virtualizedといったライブラリがこのパターンのための仕組みを提供しています。核となる考え方は一貫しています。アイテムのレンダラーを定義し、総アイテム数を渡し、ライブラリがウィンドウ処理の計算を管理します。しかし、細かな部分でつまずく人が多いのです。
First, the container needs a defined height. If the list sits inside a parent that expands to fit its children, virtualization cannot calculate which items are visible because there is no viewport boundary. You must lock the list into a fixed height or a flex container with known constraints.
Second, item sizing matters immensely. Fixed-height rows are the simplest case. The library multiplies the row height by the index and knows exactly where to position each element. Variable-height content, like chat messages with embedded images or comment threads, forces the library to measure after mount and adjust on the fly. That measurement step can cause scroll jitter if it happens too late. If your data allows it, enforce uniform heights or minimum heights. If not, use a variable-height virtualizer and accept the extra complexity.
Third, overscanning is your friend. Rendering exactly what fits on screen produces blank white strips when the user scrolls quickly. Most libraries let you render a few extra items above and below the fold. Two or three rows of overscan is usually enough to hide the seams without bloating the DOM again.
Fourth, do not ignore the key prop. In a virtualized list, items reuse DOM nodes as you scroll. Stable keys prevent React from guessing incorrectly during reconciliation and destroying state inside row components. If your list rows contain inputs, toggles, or expandable sections, bad keys will corrupt the UI state in ways that look like bugs in your data layer but are actually rendering mistakes.
One subtle trap is browser find-in-page. Because the hidden items do not exist in the DOM, the browser's search box will not see them. If your users rely on Ctrl+F to locate text inside a large list, you will need to build a custom search that operates against the dataset, not the document. Screen readers can also lose context if the list semantics are not handled carefully, so test with assistive technology and consider adding live region announcements for dynamic loading.
When You Should Skip It
Virtualization is not free. It adds dependency weight, coordinate math, and constraint overhead. If your list tops out at fifty or a hundred items, the browser can handle that without help. Render the whole thing and move on. The same applies if your list items are extremely complex individually. Virtualization saves you from thousands of nodes, but it cannot save you from one node that contains a massive chart or video element. Fix the item bloat first.
Also avoid virtualization when the list does not scroll. If you are paginating with next and previous buttons and only showing twenty items per page, there is nothing to window. The technique only earns its keep when the user expects to scroll through a large contiguous sequence.
The Real Takeaway
Virtualization is less a library choice and more a mindset. It forces you to acknowledge that the DOM is a finite resource, not an infinite canvas. Before you add it, open Chrome DevTools, record a performance profile, and confirm that layout or paint time is actually the culprit. Once you know the DOM is the bottleneck, commit to the constraints. Lock your heights, watch your keys, overscan modestly, and test your accessibility. Done right, a virtualized list turns an unusable data wall into something that feels as light as a native scroll view. The browser stops fighting, your users stop waiting, and the app finally behaves like the fast interface you intended to build.
