10万件のDOMを飼い慣らす:仮想リストによるレンダリングの極意
フロントエンドの戦場において、「DOMの肥大化」は静かなる暗殺者だ。
最初は数個のリストアイテムを表示するだけの優雅なUIだったものが、バックエンドから1万件のJSONが流れてきた瞬間、ブラウザは鈍重な鈍器へと変貌する。メモリは食い尽くされ、Layout(リフロー)とPaintのループはメインスレッドを死に至らしめる。
今日は、Reactでこの「死の淵」を回避し、いかにして大量のデータをエレガントに描画するか、その核心に触れよう。
—
なぜ「仮想DOM」だけでは救われないのか
多くのジュニアエンジニアが勘違いしていることがある。「Reactは仮想DOMを使っているから、リストがどれだけ大きくても速い」という神話だ。
残念ながら、それは誤りだ。仮想DOMは「差分計算」の効率化には寄与するが、最終的にブラウザのメモリ上に存在する実DOMのノード数までは削減してくれない。10万個の`
そこで登場するのが「仮想リスト(Virtualization)」という設計思想だ。
—
仮想リストの真髄:ビューポートという「関所」
仮想リストの仕組みは単純明快だ。「ユーザーが見ていないものは、存在しないものとせよ」。
画面(ビューポート)に映る数件のアイテムだけをDOMに配置し、スクロール量に応じて中身を動的に差し替える。これにより、リストが100件だろうが100万件だろうが、ブラウザが保持するDOMノード数は「画面に収まる数+α」で一定に保たれる。
実装の勘所:react-windowの採用
ゼロから実装するのは車輪の再発明だが、`react-window`はその中でも最も軽量で、Reactの設計思想に忠実なライブラリだ。
import React from ‘react’;
import { FixedSizeList as List } from ‘react-window’;
// アイテムコンポーネントは純粋であるべき。
// 必要以上に計算させないことが鉄則だ。
const Row = ({ index, style }) => (
);
const App = () => (
{Row}
);
—
現場で必ず直面する「落とし穴」と回避策
理論通りにいかないのが現場の面白いところだ。上級エンジニアとして、以下の3つの「壁」をどう乗り越えるかが問われる。
1. 非同期データの競合(Race Conditions)
大量のデータを無限スクロールで読み込む際、APIのレスポンスが順不同で返ってくるとUIが崩壊する。`useRef`で最新の「データフェッチID」を保持し、古いレスポンスを破棄するガード節を入れるのが定石だ。
2. コンポーネントの再レンダリング地獄
`react-window`で描画されるアイテムは頻繁に再レンダリングされる。`React.memo`でラップするのは当然として、propsに関数やオブジェクトを渡す際は`useCallback`や`useMemo`をケチるな。参照の同一性が崩れるだけで、リスト全体が再評価されるコストは馬鹿にならない。
3. 可変高アイテムの難しさ
`FixedSizeList`(固定高)は楽だが、テキスト量によって高さが変わる場合は`VariableSizeList`を使う必要がある。しかし、これには「すべての行の高さをキャッシュする」という計算コストが伴う。頻繁な更新が必要なリストでは、このキャッシュ計算がボトルネックになることを忘れてはならない。
—
アーキテクチャとしての「断捨離」
最後に、技術的なアドバイスを一つ。「本当に10万件を一度に表示する必要があるか?」を常に自問自答してほしい。
人間は一度に10万件のデータを認知できない。仮想リストは強力な武器だが、それはあくまで「限界まで詰め込むための緩和策」に過ぎない。本来であれば、サーバーサイドでのフィルタリング、検索機能の強化、あるいは「もっとも重要な100件」を提示するUX設計こそが、最高峰のアーキテクチャだ。
Reactの真の力は、フレームワークを駆使することではなく、ブラウザの限界を理解し、いかに「描画しない勇気」を持つかにある。
さあ、DOMノードを削ぎ落とし、メインスレッドを解放せよ。それが、真に堅牢なWebアプリケーションへの第一歩だ。

コメント