数千件のリストをDOMで溺れさせない:仮想スクロール(Windowing)の真髄
フロントエンド開発の現場で、「リストアイテムが数千件ある」という要件を聞いたとき、皆さんはどう感じますか?正直、冷や汗が出ませんか?
初心者が真っ先にやるのは、単純な `ul` と `li` のループ生成です。「とりあえず動くからいいや」と放置した結果、ブラウザのメインスレッドは悲鳴を上げ、スクロールはカクつき、メモリ使用量は右肩上がり。最終的にはユーザーから「このアプリ、重くて使えないよ」という一言が飛んでくる。これが、最適化を怠った末路です。
今日は、そんなDOMの過積載から解放されるための「仮想スクロール(Windowing)」という技術について、現場の知見を交えて深掘りしていきます。
—
なぜ、素直にリストを全レンダリングしてはいけないのか?
ブラウザの仕組みを少し考えてみましょう。ブラウザはDOMノードを生成し、スタイルを計算し、レイアウトを組み立て、ペイントします。
例えば、10,000件の `li` タグをDOMに挿入したとします。ブラウザは10,000個のノードをメモリに抱え、CSSの計算を行い、スクロールするたびにリフローやリペイントのコストを支払います。DOMノード数は、ブラウザのパフォーマンスにおける最大の敵の一つです。
仮想スクロールのコンセプトは非常にシンプルです。
「今、ユーザーが見ている画面(Viewport)の分だけDOMをレンダリングし、スクロールに合わせて中身を入れ替える」。
これだけで、DOMノード数は数千から数十へと激減します。これこそが、大規模リストをスムーズに扱うための現代の「正攻法」です。
—
実装の仕組み:スクロールの「影武者」を作る
仮想スクロールを実装する際、最も重要なのは「スクロールの高さ」です。
画面に見えている10個分しかDOMがないのに、スクロールバーの長さは10,000件分である必要があります。これを実現するために、以下の2段構造をとります。
1. コンテナ要素: `overflow: auto` を設定し、実際の表示領域として機能させる。
2. パディング要素(スペーサー): コンテナの中に、本来必要な全リスト分の高さを計算して設定した、透明な高さを持つ要素を配置する。
この「巨大な高さを持つ箱」の中で、現在位置に応じたデータだけを絶対配置(`absolute`)で描画する。これが仮想スクロールの核となるロジックです。
—
【実践】コピペで使える仮想スクロールの基本実装
今回はライブラリに頼らず、Reactなどのフレームワークでも応用可能なロジックをプレーンなJavaScriptで解説します。
/
- 仮想スクロールの簡易実装例
- @param {HTMLElement} container – スクロール対象のコンテナ
- @param {number} totalItems – リストの総数
- @param {number} itemHeight – 各アイテムの高さ
/
function setupVirtualScroll(container, totalItems, itemHeight) {
// コンテナのCSS設定を保証
container.style.position = ‘relative’;
container.style.overflowY = ‘auto’;
// 1. スペーサー(全リスト分の高さを確保する見えない要素)
const spacer = document.createElement(‘div’);
spacer.style.height = `${totalItems itemHeight}px`;
container.appendChild(spacer);
// 2. アイテム描画用コンテナ
const viewport = document.createElement(‘div’);
viewport.style.position = ‘absolute’;
viewport.style.top = ‘0’;
viewport.style.width = ‘100%’;
container.appendChild(viewport);
container.addEventListener(‘scroll’, () => {
const scrollTop = container.scrollTop;
// 現在のスクロール位置から、表示すべき開始・終了インデックスを計算
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
totalItems – 1,
Math.floor((scrollTop + container.clientHeight) / itemHeight)
);
// 3. 必要な分だけDOMを生成して入れ替える
renderVisibleItems(viewport, startIndex, endIndex, itemHeight);
});
}
function renderVisibleItems(container, start, end, itemHeight) {
container.innerHTML = ”; // 簡易的なクリア(実務ではDOM Diffingを使うのがベター)
for (let i = start; i <= end; i++) { const item = document.createElement('div'); item.textContent = `アイテム番号: ${i}`; item.style.position = 'absolute'; item.style.top = `${i itemHeight}px`; // 正しい位置に配置 item.style.height = `${itemHeight}px`; container.appendChild(item); } } ---
現場で生き残るための「プラスアルファ」の知見
上記のコードはあくまで概念実証ですが、実務で使う場合は以下の点に注意してください。
- バッファ(オーバーキャン)の重要性: スクロールした瞬間にアイテムがカクッと出現するとチープに見えます。表示範囲の上下に「少し多め(例:画面高さの半分程度)」のアイテムを先読みして描画しておくと、スクロールが非常に滑らかになります。
- DOMの再利用(Recycling): `innerHTML = ”` で全削除して再生成するのはコストが高いです。既存のDOMノードを使い回し、中身のテキストだけを書き換える手法を取ると、さらにパフォーマンスが向上します。
- ライブラリの検討: 自分でゼロから実装するのは勉強になりますが、複雑なケース(可変長の高さなど)に対応するのは至難の業です。実務では `react-window` や `TanStack Virtual` といった、枯れたライブラリを使うのが賢明です。
最後に:完璧を追い求めすぎないこと
仮想スクロールは強力な武器ですが、銀の弾丸ではありません。数百件程度のリストであれば、現代のブラウザは十分に高速です。無理に仮想スクロールを導入して、コードの複雑性を増す必要はありません。
「本当にパフォーマンスの問題が発生しているのか?」 を計測(Chrome DevToolsのPerformanceパネルを確認)し、必要だと判断したときに初めてこの技術を投入する。その冷静な判断力こそが、シニアエンジニアへの第一歩です。
皆さんのプロジェクトが、サクサクと快適に動くリストで溢れることを期待しています。何か詰まったら、いつでも相談してください。

コメント