数千件のリストを突きつけられたとき、DOMを焼き尽くさないための「仮想スクロール」という処方箋
フロントエンドの現場で「リストの表示」は、最も頻繁に遭遇するUIパターンの一つです。しかし、APIから返ってきた数千件のデータを、安易に `map()` で `
「とりあえず表示できたからOK」——そう思っているあなた。ブラウザのデベロッパーツールで「パフォーマンス」タブを覗いてみてください。数千のDOMノードが生成された瞬間、ブラウザのメインスレッドは悲鳴を上げ、メモリ消費量は跳ね上がり、スクロールはカクつき始めます。
今日は、DOMの限界と向き合い、現場で生き残るための「仮想スクロール」という技術的最適化について、泥臭い現実と解決策を紐解いていきます。
—
なぜ数千の ` ` は悪夢なのか?
ブラウザにとって、DOMノードを生成・管理するのは決して「タダ」ではありません。
1. メモリ消費: 数千件のノードは、それだけで数百メガバイトのメモリを食う可能性があります。
2. レイアウト・再計算(Reflow): リストの一部が更新されるたび、ブラウザは数千の要素を対象に計算をやり直す可能性があります。
3. イベントリスナー: もし各 `
ブラウザは「表示されていない要素」に対しても、律儀にメモリを確保し、スタイルを計算し続けます。私たちがやるべきことは、「今、画面に見えているものだけをDOMにする」という極めてシンプルな(しかし実装には工夫が要る)戦略への切り替えです。
—
仮想スクロール(Virtual Scrolling)の概念
仮想スクロールの本質は、「スクロール領域という巨大な箱の中に、表示に必要な分だけのDOMを使い回す」という仕組みです。
- コンテナ: 高さを固定したスクロール可能な親要素。
- パッディング: 全体が表示されているかのように見せるための、目に見えない余白。
- アクティブなノード: 画面の表示範囲(viewport)にある数個の要素だけをレンダリングし、スクロールに合わせて動的に中身を入れ替える。
これにより、DOMの総数はデータが100件でも10万件でも、常に「10〜20件程度」に固定されます。パフォーマンスは劇的に向上し、UIの滑らかさは保証されるのです。
—
実践:Vanilla JSによる仮想スクロールのミニマム実装
ライブラリを使わず、まずはその仕組みを理解しましょう。以下は、数千件のリストを軽快に扱うための概念コードです。そのままエディタにコピペして、動作の軽さを体感してください。
/
- 仮想スクロールの極めてシンプルな実装例
/
const container = document.getElementById(‘list-container’);
const totalItems = 10000; // 全データ数
const itemHeight = 40; // 1行の高さ(px)
const visibleCount = 10; // 画面に表示する個数
// コンテナ全体の高さを設定(スクロールバーを正しく表示するため)
container.style.height = `${visibleCount itemHeight}px`;
const spacer = document.createElement(‘div’);
spacer.style.height = `${totalItems itemHeight}px`;
container.appendChild(spacer);
// リスト表示用の子要素(これだけを使い回す)
const listWrapper = document.createElement(‘div’);
container.appendChild(listWrapper);
container.addEventListener(‘scroll’, () => {
const scrollTop = container.scrollTop;
// 現在のスクロール位置から表示すべきデータの開始インデックスを計算
const startIndex = Math.floor(scrollTop / itemHeight);
// 表示範囲のデータを取得してDOMを更新
renderItems(startIndex);
});
function renderItems(startIndex) {
listWrapper.innerHTML = ”; // 簡易的な更新。実務ではDOMの再利用を推奨
for (let i = 0; i < visibleCount; i++) {
const index = startIndex + i;
if (index >= totalItems) break;
const li = document.createElement(‘div’);
li.style.height = `${itemHeight}px`;
li.textContent = `アイテム番号: ${index + 1}`;
listWrapper.appendChild(li);
}
// スクロール位置に合わせてリスト位置を調整
listWrapper.style.transform = `translateY(${startIndex itemHeight}px)`;
}
—
シニアエンジニアからのアドバイス
上記のコードはあくまで概念実証です。実務で導入する際は、以下のポイントを必ず考慮してください。
1. DOMの再利用(Recycling): 上記のサンプルでは `innerHTML = ”` を使っていますが、頻繁なDOM破棄・生成はガベージコレクションを誘発します。既存の要素を `display: none` で隠したり、テキストだけを書き換える手法がより効率的です。
2. ライブラリの活用: 自前での実装は、スクロールの慣性やウィンドウリサイズへの対応など、エッジケースの沼にハマりがちです。まずは `react-window` や `vue-virtual-scroller` といった、枯れたライブラリを使うのがプロの判断です。
3. アクセシビリティの考慮: DOMを隠すと、スクリーンリーダーがリスト全体を認識できなくなることがあります。`aria-setsize` や `aria-posinset` を適切に設定し、視覚障害を持つユーザーにも優しい設計を心がけましょう。
まとめ
数千件のリストを扱う際、「すべてをDOMに乗せる」のは怠慢です。ブラウザの性能を過信せず、表示に必要な最小限のリソースだけを管理する。この「引き算の思想」こそが、パフォーマンスを追求するフロントエンドエンジニアの矜持です。
まずは、目の前のリストが本当に数千件のDOMを必要としているのか、疑ってみることから始めてください。その疑いこそが、あなたのアプリケーションをよりプロフェッショナルなものへと進化させるはずです。

コメント