2026年版 高速JavaScript ListView: ウィンドウイング、仮想化、パフォーマンス最適化

TanStack Virtual、react-virtuoso、react-windowを使用したJavaScriptリスト仮想化を習得します。ウィンドウイング技術で10万件以上のアイテムを60fpsでレンダリングするコード例を解説します。

高速スクロールパフォーマンスのためのウィンドウイング技術を用いたJavaScriptリスト仮想化

仮想化を行わずにJavaScript ListViewで数千件のアイテムをレンダリングすると、ブラウザは数千のDOMノードを作成します。その結果、初期レンダリングの遅延、スクロールのカクつき、メモリ消費の増大が発生し、モバイルデバイスではクラッシュすることもあります。2026年における高速JavaScript ListView実装では、ウィンドウイングによってこの問題を解決します。ビューポートに表示される10〜20件のアイテムのみをレンダリングし、全アイテムを含むスクロール可能な領域をシミュレートする手法です。

ウィンドウイングの本質

ウィンドウイングは、表示されているアイテムと小さなバッファ(オーバースキャン)のみをレンダリングし、リストサイズに関係なくDOMノードを10,000から約20に削減します。

大規模リストでブラウザパフォーマンスが低下する理由

すべてのDOMノードはメモリを消費し、レイアウト計算時にブラウザがその位置を追跡する必要があります。適度に複雑なマークアップ(アバター、2つのテキストスパン、ボタン)を持つ10,000件のリストは、40,000以上のDOMノードを作成します。Chromeのレンダリングエンジンは、スクロールイベントのたびにそれらすべてのスタイルと位置を再計算します。

その結果、初期レンダリングに2〜4秒かかり、スクロールは15fpsまで低下し、メモリ使用量は500MB以上に増加します。モバイルSafariでは、タブがクラッシュすることも珍しくありません。

仮想化は、一定のDOMノード数を維持することでこの問題を解決します。データ配列に100件含まれていても100,000件含まれていても、レンダリングされるDOMは約20〜30ノードに留まり、メモリは一定でスクロールパフォーマンスは60fpsを維持します。

VirtualizedList.tsxtypescript
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'

interface Item {
  id: string
  name: string
  email: string
}

export function VirtualizedList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null)
  
  // 推定行高でバーチャライザーインスタンスを作成
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60, // ピクセル単位の推定高さ
    overscan: 5, // ビューポートの上下に5つの追加アイテムをレンダリング
  })

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualRow) => {
          const item = items[virtualRow.index]
          return (
            <div
              key={item.id}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              <div className="flex items-center gap-3 p-3 border-b">
                <div className="w-10 h-10 rounded-full bg-gray-200" />
                <div>
                  <p className="font-medium">{item.name}</p>
                  <p className="text-sm text-gray-500">{item.email}</p>
                </div>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

この実装は、表示される行とオーバースキャンバッファのみをレンダリングします。外側のコンテナがスクロール可能な総高さを設定し、すべてのアイテムがDOMに存在するかのようにスクロールバーを動作させます。

TanStack Virtual: 2026年のヘッドレスアプローチ

TanStack Virtualは、フレームワークに依存しない仮想化プリミティブを提供します。react-windowやreact-virtuosoとは異なり、事前構築されたコンポーネントは提供せず、どのアイテムをレンダリングし、どこに配置するかを計算するフックのみを提供します。このヘッドレスアーキテクチャにより、スタイリングとDOM構造を完全に制御できます。

現在のバージョン(3.13)は、React、Vue、Solid、Svelte、およびバニラJavaScriptをサポートしています。主な設定オプションは以下の通りです。

オプション目的デフォルト
countアイテムの総数必須
estimateSize推定アイテム高さ/幅を返す関数必須
overscanビューポート外にレンダリングする追加アイテム数1
getScrollElementスクロール可能なコンテナを返す必須
measureElement動的サイズ用のカスタム測定関数自動

overscanの値はスクロールの滑らかさに直接影響します。低すぎる値(0〜2)に設定すると、高速スクロール時に白いフラッシュが発生します。高すぎる値(20以上)に設定すると、仮想化のメリットが失われます。ほとんどのアプリケーションでは、5〜10が最適なバランスを提供します。

DynamicHeightList.tsxtypescript
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef, useCallback } from 'react'

interface Message {
  id: string
  content: string
  timestamp: number
}

export function ChatList({ messages }: { messages: Message[] }) {
  const parentRef = useRef<HTMLDivElement>(null)
  
  // measureElementで動的な高さ測定を有効化
  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // 大まかな推定、実際は後で測定
    overscan: 8,
  })

  return (
    <div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => {
          const message = messages[virtualRow.index]
          return (
            <div
              key={message.id}
              data-index={virtualRow.index}
              ref={virtualizer.measureElement} // 自動測定を有効化
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              <div className="p-4 border-b">
                <p className="text-sm text-gray-500">
                  {new Date(message.timestamp).toLocaleTimeString()}
                </p>
                <p className="mt-1">{message.content}</p>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

measureElement refは、レンダリング後の自動高さ測定を有効にします。これにより、手動で高さを計算することなく、可変コンテンツ長を処理できます。

react-virtuoso: 機能豊富な代替選択肢

react-virtuosoは正反対のアプローチを取ります。バッテリー同梱型のコンポーネントライブラリです。TanStack Virtualがスクロールコンテナと配置ロジックの構築を必要とするのに対し、react-virtuosoはこれらを内部で処理します。

トレードオフはバンドルサイズ(TanStack Virtualの12KBに対して37KB)ですが、グループ化されたリスト、スティッキーヘッダー、逆スクロール付きチャットインターフェースなどの一般的なパターンに対するボイラープレートが大幅に削減されます。

VirtuosoList.tsxtypescript
import { Virtuoso } from 'react-virtuoso'

interface User {
  id: string
  name: string
  department: string
}

export function UserDirectory({ users }: { users: User[] }) {
  return (
    <Virtuoso
      style={{ height: '600px' }}
      data={users}
      itemContent={(index, user) => (
        <div className="flex items-center gap-3 p-4 border-b hover:bg-gray-50">
          <div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
            {user.name.charAt(0)}
          </div>
          <div>
            <p className="font-medium">{user.name}</p>
            <p className="text-sm text-gray-500">{user.department}</p>
          </div>
        </div>
      )}
    />
  )
}

より少ないコードで同じ仮想化結果を達成できます。react-virtuosoは自動的にアイテムを測定し、動的な高さを処理し、スクロール位置の永続化を提供します。

Reactパフォーマンスに関する面接準備において、各ライブラリをいつ選択すべきかを理解していることは、アーキテクチャへの認識を示す機会となります。TanStack Virtualは、既にTanStack Tableを使用しているアプリケーションや、フレームワークに依存しないコードが必要な場合に適しています。react-virtuosoは、バンドルサイズよりも開発速度を優先するチームに適しています。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

一般的な落とし穴と解決策

画像によるスクロール位置のジャンプ

画像は非同期で読み込まれ、初期測定後に行の高さが変化します。バーチャライザーはこの変化を予測できず、下のコンテンツがずれてしまいます。

解決策は、aspect-ratioまたは固定寸法を使用して画像のスペースを予約することです。

ImageListItem.tsxtypescript
interface ImageItem {
  id: string
  src: string
  title: string
}

export function ImageListItem({ item }: { item: ImageItem }) {
  return (
    <div className="flex gap-4 p-4 border-b">
      {/* 読み込み状態に関係なく80x80pxのスペースを予約 */}
      <div className="w-20 h-20 flex-shrink-0 bg-gray-100">
        <img
          src={item.src}
          alt={item.title}
          className="w-full h-full object-cover"
          loading="lazy"
        />
      </div>
      <p className="font-medium">{item.title}</p>
    </div>
  )
}

スクロール中の状態消失

仮想化されたアイテムは、ビューからスクロールアウトするとアンマウントされます。ローカル状態(フォーム入力、展開されたアコーディオン)は消えてしまいます。

解決策は、アイテムIDでキー付けされた状態を親コンポーネントまたは状態管理ライブラリに持ち上げることです。

ExpandableList.tsxtypescript
import { useState, useCallback } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'

interface FAQ {
  id: string
  question: string
  answer: string
}

export function FAQList({ faqs }: { faqs: FAQ[] }) {
  // 状態は親に存在し、行のアンマウント後も保持される
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
  const parentRef = useRef<HTMLDivElement>(null)
  
  const toggleExpanded = useCallback((id: string) => {
    setExpandedIds(prev => {
      const next = new Set(prev)
      if (next.has(id)) {
        next.delete(id)
      } else {
        next.add(id)
      }
      return next
    })
  }, [])
  
  const virtualizer = useVirtualizer({
    count: faqs.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60,
    overscan: 5,
  })

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualRow) => {
          const faq = faqs[virtualRow.index]
          const isExpanded = expandedIds.has(faq.id)
          return (
            <div
              key={faq.id}
              data-index={virtualRow.index}
              ref={virtualizer.measureElement}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              <button
                onClick={() => toggleExpanded(faq.id)}
                className="w-full text-left p-4 border-b"
              >
                <p className="font-medium">{faq.question}</p>
                {isExpanded && (
                  <p className="mt-2 text-gray-600">{faq.answer}</p>
                )}
              </button>
            </div>
          )
        })}
      </div>
    </div>
  )
}

不安定なキーによる再レンダリング

配列インデックスをキーとして使用すると、リストが並び替えられるまでは機能します。アイテムが間違った状態を受け取り、視覚的な不具合や無駄なレンダリングが発生します。

常にデータ自体から安定した一意の識別子を使用する必要があります。

typescript
// 正しい: データからの安定したキー
{virtualizer.getVirtualItems().map((virtualRow) => {
  const item = items[virtualRow.index]
  return <div key={item.id}>...</div> // データベース/APIからのid
})}

// 間違い: インデックスはリストの並び替えで変化する
{virtualizer.getVirtualItems().map((virtualRow, index) => {
  return <div key={index}>...</div> // 並び替え時にバグを引き起こす
})}

2026年プロジェクト向けライブラリ比較

適切な仮想化ライブラリの選択は、プロジェクト要件によって異なります。この比較は2026年のエコシステムを反映しています。

基準TanStack Virtualreact-virtuosoreact-window
バンドルサイズ12KB37KB6KB
動的な高さ手動設定自動非対応
TypeScriptネイティブネイティブ@typesパッケージ
フレームワークサポートReact, Vue, Solid, SvelteReactのみReactのみ
メンテナンスアクティブ(2026年)アクティブ(2026年)メンテナンスモード
学習コスト高い低い最も低い

react-windowは、バンドルサイズが重要な単純な固定高さリストには依然として有効です。ただし、メンテナンスモードのステータスと動的な高さサポートの欠如により、複雑な要件を持つ新規プロジェクトには不向きです。

パフォーマンスの高いReactアプリケーションの構築において、仮想化のトレードオフを理解することは、面接官が求めるパフォーマンス意識を示します。

パフォーマンス改善の測定

仮想化のメリットを定量化するには、一貫した測定方法論が必要です。Chrome DevToolsのPerformanceパネルが関連するメトリクスを提供します。

仮想化前後の主要な測定項目は以下の通りです。

  • First Contentful Paint (FCP): 最初のコンテンツがレンダリングされるまでの時間。仮想化は初期DOM作成を制限することでこれを短縮します。
  • Total Blocking Time (TBT): 読み込み中のメインスレッドブロッキング。DOMノードが少ないほど、スタイル/レイアウト計算が少なくなります。
  • メモリ使用量: Memoryパネルでヒープサイズのスナップショットを取得。リスト長に関係なく一定であるべきです。
  • スクロールfps: スクロール中にPerformanceトレースを記録。フレームレートは60fpsを維持する必要があります。
PerformanceMonitor.tsxtypescript
import { useEffect, useRef } from 'react'

export function useScrollFPS(containerRef: React.RefObject<HTMLElement>) {
  const frameCountRef = useRef(0)
  const lastTimeRef = useRef(performance.now())
  
  useEffect(() => {
    const container = containerRef.current
    if (!container) return
    
    let rafId: number
    
    const measureFrame = () => {
      frameCountRef.current++
      const now = performance.now()
      
      if (now - lastTimeRef.current >= 1000) {
        console.log(`Scroll FPS: ${frameCountRef.current}`)
        frameCountRef.current = 0
        lastTimeRef.current = now
      }
      
      rafId = requestAnimationFrame(measureFrame)
    }
    
    const handleScroll = () => {
      if (!rafId) {
        rafId = requestAnimationFrame(measureFrame)
      }
    }
    
    container.addEventListener('scroll', handleScroll, { passive: true })
    
    return () => {
      container.removeEventListener('scroll', handleScroll)
      cancelAnimationFrame(rafId)
    }
  }, [containerRef])
}

このフックは開発中にスクロールFPSをログ出力します。本番アプリケーションでは、Sentry Performanceやカスタムテレメトリなどの監視ツールを使用する必要があります。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

JavaScriptリストパフォーマンスの重要なポイント

  • 仮想化はDOMノードを数千から約20に削減し、データサイズに関係なくメモリとスクロールパフォーマンスを一定に保ちます
  • TanStack Virtual(12KB)は、React、Vue、Solid、Svelteで最大限の制御を可能にするヘッドレスプリミティブを提供します
  • react-virtuoso(37KB)は、自動動的高さ処理付きのバッテリー同梱型コンポーネントを提供します
  • react-windowは2022年以降メンテナンスモードであり、動的な高さサポートがありません
  • オーバースキャンは5〜10に設定すると、仮想化のメリットを損なうことなく最適なスクロールの滑らかさが得られます
  • 画像の寸法を予約して、非同期読み込み中のスクロール位置ジャンプを防止します
  • 仮想化された行はスクロール中にアンマウントされるため、展開可能/編集可能な状態は親コンポーネントに持ち上げます
  • 並び替え時の状態の不一致を防ぐため、配列インデックスではなくデータIDからの安定したキーを使用します
  • 実装前後でFCP、TBT、メモリ、スクロールFPSを測定し、改善を定量化します
今日のチャレンジ

Node.js / NestJS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月15日 更新

タグ

#javascript
#performance
#virtualization
#react
#typescript

共有

関連記事