【実務・中級編】大規模リストのパフォーマンス最適化(仮想スクロール) – HTML実践ガイド

数千件のリストを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パネルを確認)し、必要だと判断したときに初めてこの技術を投入する。その冷静な判断力こそが、シニアエンジニアへの第一歩です。

皆さんのプロジェクトが、サクサクと快適に動くリストで溢れることを期待しています。何か詰まったら、いつでも相談してください。

コメント

タイトルとURLをコピーしました