【実務・中級編】大量リストのレンダリングパフォーマンス – HTML実践ガイド

数千件のリストを突きつけられたとき、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を必要としているのか、疑ってみることから始めてください。その疑いこそが、あなたのアプリケーションをよりプロフェッショナルなものへと進化させるはずです。

  • コメント

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