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

仮想化を行わずに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を維持します。
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が最適なバランスを提供します。
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)ですが、グループ化されたリスト、スティッキーヘッダー、逆スクロール付きチャットインターフェースなどの一般的なパターンに対するボイラープレートが大幅に削減されます。
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または固定寸法を使用して画像のスペースを予約することです。
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でキー付けされた状態を親コンポーネントまたは状態管理ライブラリに持ち上げることです。
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>
)
}不安定なキーによる再レンダリング
配列インデックスをキーとして使用すると、リストが並び替えられるまでは機能します。アイテムが間違った状態を受け取り、視覚的な不具合や無駄なレンダリングが発生します。
常にデータ自体から安定した一意の識別子を使用する必要があります。
// 正しい: データからの安定したキー
{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 Virtual | react-virtuoso | react-window |
|---|---|---|---|
| バンドルサイズ | 12KB | 37KB | 6KB |
| 動的な高さ | 手動設定 | 自動 | 非対応 |
| TypeScript | ネイティブ | ネイティブ | @typesパッケージ |
| フレームワークサポート | React, Vue, Solid, Svelte | Reactのみ | Reactのみ |
| メンテナンス | アクティブ(2026年) | アクティブ(2026年) | メンテナンスモード |
| 学習コスト | 高い | 低い | 最も低い |
react-windowは、バンドルサイズが重要な単純な固定高さリストには依然として有効です。ただし、メンテナンスモードのステータスと動的な高さサポートの欠如により、複雑な要件を持つ新規プロジェクトには不向きです。
パフォーマンスの高いReactアプリケーションの構築において、仮想化のトレードオフを理解することは、面接官が求めるパフォーマンス意識を示します。
パフォーマンス改善の測定
仮想化のメリットを定量化するには、一貫した測定方法論が必要です。Chrome DevToolsのPerformanceパネルが関連するメトリクスを提供します。
仮想化前後の主要な測定項目は以下の通りです。
- First Contentful Paint (FCP): 最初のコンテンツがレンダリングされるまでの時間。仮想化は初期DOM作成を制限することでこれを短縮します。
- Total Blocking Time (TBT): 読み込み中のメインスレッドブロッキング。DOMノードが少ないほど、スタイル/レイアウト計算が少なくなります。
- メモリ使用量: Memoryパネルでヒープサイズのスナップショットを取得。リスト長に関係なく一定であるべきです。
- スクロールfps: スクロール中にPerformanceトレースを記録。フレームレートは60fpsを維持する必要があります。
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-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年9月15日 更新
タグ
共有
関連記事

2026年版 高性能JavaScript ListViewの実装:仮想化と最適化テクニック
JavaScriptにおけるリストの仮想化技術を徹底解説。TanStack Virtual、react-virtuoso、react-windowを活用した大規模データの効率的なレンダリング手法と面接対策を網羅します。

NestJS + Prisma:Node.js のためのモダンなバックエンドスタック
NestJS と Prisma によるモダンなバックエンド API 構築の完全ガイドです。セットアップ、モデル、サービス、トランザクション、ベストプラクティスを解説します。

Node.jsバックエンド面接質問:完全ガイド2026
Node.jsバックエンド面接で最も頻出の25問。Event Loop、async/await、Streams、クラスタリング、パフォーマンスを詳細な回答で解説します。