このドキュメントは、React アプリケーションで大規模なリストや無限スクロールを実装する際の主要なライブラリを比較しています。react-virtualized、react-window、react-virtual などの仮想化ライブラリから、react-infinite、react-list などの従来の無限スクロールコンポーネントまで、それぞれのアーキテクチャ、API デザイン、メンテナンス状況を分析します。パフォーマンス、柔軟性、開発者体験の観点から、プロジェクトに適した技術選定を行うための指針を提供します。
大規模なリストを扱う際、すべてのアイテムを一度にレンダリングすると、ブラウザが重くなりユーザー体験が損なわれます。これを解決するため、表示されている部分だけを描画する「ウィンドウイング(仮想化)」技術や、スクロールに応じてデータを読み込む「無限スクロール」実装が必要になります。
今回は、react-infinite、react-list、react-virtual、react-virtualized、react-window、react-window-infinite-loader の 6 つのパッケージを比較し、それぞれの技術的特徴と適切な使いどころを明確にします。
ライブラリを選ぶ際、まず「完成されたコンポーネントを提供するか」それとも「ロジックだけを提供するか(ヘッドレス)」という違いを理解する必要があります。
react-virtualized と react-window は、高度に最適化されたコンポーネントを提供します。
// react-window: 完成されたコンポーネントを使用
import { FixedSizeList } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const List = () => (
<FixedSizeList height={600} itemCount={1000} itemSize={35}>
{Row}
</FixedSizeList>
);
react-virtual はヘッドレスアプローチを採用しています。
// react-virtual: 仮想化ロジックだけをフックで取得
import { useVirtual } from 'react-virtual';
import { useRef } from 'react';
const List = () => {
const parentRef = useRef();
const virtual = useVirtual({
size: 1000,
parentRef,
estimateSize: () => 35,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtual.totalSize}px` }}>
{virtual.virtualItems.map(item => (
<div key={item.key} style={{ position: 'absolute', top: 0, transform: `translateY(${item.start}px)` }}>
Row {item.index}
</div>
))}
</div>
</div>
);
};
react-infinite と react-list は、より単純なコンポーネント型ですが、内部最適化は古いです。
// react-infinite: 単純なコンポーネント構成
import Infinite from 'react-infinite';
const List = () => (
<Infinite elementHeight={50} containerHeight={600}>
<div>Item 1</div>
<div>Item 2</div>
{/* 子要素をすべて渡す必要がある場合が多い */}
</Infinite>
);
ライブラリの長期的なメンテナンスは、プロジェクトの安定性に直結します。
react-virtualized は事実上メンテナンスモードに入っています。
react-window への移行が推奨されています。react-window が現在のデファクトスタンダードです。
react-virtualized の欠点を解消するために作られました。react-infinite と react-list は更新頻度が低いです。
react-virtual は活発にメンテナンスされています。
リストが無限に続く場合(ページネーションなしでスクロールするとデータが追加される)、追加のロジックが必要になります。
react-window-infinite-loader は、react-window のために作られた専用ラッパーです。
react-window と組み合わせることで、高性能な無限スクロールが実現できます。// react-window-infinite-loader: 無限ローディングの管理
import InfiniteLoader from 'react-window-infinite-loader';
import { FixedSizeList } from 'react-window';
const isItemLoaded = (index) => !!items[index];
const loadMoreItems = (startIndex, stopIndex) => {
// API を叩いてデータを取得し、items 配列を更新する
};
const List = () => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={items.length}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
itemCount={items.length}
itemSize={35}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
);
react-virtual にも同様の機能がありますが、より低レベルです。
// react-virtual: 自前でローディングロジックを制御
const virtual = useVirtual({
size: items.length,
parentRef,
estimateSize: () => 35,
overscan: 5,
});
// onScroll などで末端に達したことを検知し、loadMoreItems を発火させる必要がある
react-infinite は無限スクロールが主目的です。
// react-infinite: 無限スクロールイベントをトリガーに使用
<Infinite
useWindowAsScrollContainer
onInfiniteLoad={loadMoreItems}
loadingSpinnerContainer={<div>Loading...</div>}
>
{items.map(item => <div key={item.id}>{item.name}</div>)}
</Infinite>
大量のデータを扱う場合、いかに DOM 操作を減らすかが鍵になります。
| 特徴 | react-window / react-virtualized | react-virtual | react-infinite / react-list |
|---|---|---|---|
| レンダリング | 表示領域のみレンダリング(ウィンドウイング) | 表示領域のみレンダリング | 全要素を保持しようとする傾向 |
| DOM 更新 | 最小限に抑えられた更新 | 開発者の実装に依存 | 頻繁な更新が発生しやすい |
| スクロール | 専用コンテナでのスクロール制御 | 任意のコンテナで制御可能 | ウィンドウスクロールに依存しがち |
| メモリ | 効率的 | 効率的 | 高負荷になりやすい |
react-window は、アイテムのサイズが固定か変動かに応じてコンポーネントを使い分けます。
FixedSizeList: 高速。すべてのアイテムが同じ高さの場合。VariableSizeList: 柔軟。アイテムの高さが異なる場合(計算コストが少し増える)。// 固定サイズの場合(最速)
<FixedSizeList itemSize={50} ... />
// 可変サイズの場合(柔軟)
<VariableSizeList itemSize={index => getHeight(index)} ... />
react-virtualized も同様の機能を持ちますが、API が複雑です。
List、Grid、Table など用途別にコンポーネントが細分化されています。実装のしやすさや、デザインのカスタマイズ自由度も重要な選定基準です。
react-window はバランスが良いです。
react-virtual はカスタマイズ性が最高です。
// react-virtual: 自由な HTML 構造が可能
<div ref={parentRef}>
{virtualItems.map(item => (
<div key={item.key} style={{ ...item.measurements }}>
{/* 中に何をいれても自由 */}
</div>
))}
</div>
react-infinite は簡単ですが制限があります。
プロジェクトの要件に応じて、以下のように選定するのが合理的です。
| 要件 | 推奨ライブラリ | 理由 |
|---|---|---|
| 標準的なリスト | react-window | 軽量、高速、メンテナンスが活発 |
| 無限スクロール | react-window + react-window-infinite-loader | 組み合わせることで最強の性能と機能 |
| 完全なカスタマイズ | react-virtual | UI 制約がなく、ロジックのみ借用できる |
| レガシー維持 | react-virtualized | 既存実装がある場合のみ。新規は非推奨 |
| シンプルな実装 | react-infinite | 非推奨。データ量少ならありだがリスク大 |
| 他フレームワーク | react-virtual | 核心ロジックがフレームワーク非依存 |
2024 年以降の React プロジェクトにおいて、リストの仮想化が必要な場合、react-window が最も堅実な選択です。特に react-window-infinite-loader と組み合わせることで、無限スクロールの課題も解決できます。API が直感的で、コミュニティのサポートも厚いため、トラブルが起きた際の解決も容易です。
一方、デザインシステムを自作していたり、リスト以外の複雑な仮想化(例えば、巨大なカレンダーやグリッド)が必要な場合は、react-virtual が優れた選択肢となります。ヘッドレスであるため、コンポーネントの設計に縛られず、パフォーマンスとデザインを両立できます。
react-virtualized、react-infinite、react-list については、既存プロジェクトのメンテナンス以外で新規に採用する理由はほとんどありません。これらは過去の技術的アプローチを反映しており、現代の React のパフォーマンス要件やエコシステムに最適化されていません。
技術選定では「できること」だけでなく「維持できること」も重要です。アクティブにメンテナンスされ、現代の Web 標準に沿ったライブラリを選ぶことが、長期的なプロジェクトの成功につながります。
シンプルな実装で十分な場合や、非常に古いコードベースの維持が必要な場合に限り考慮します。機能性やパフォーマンス面で現代的な仮想化ライブラリに劣るため、新規開発では他の選択肢を優先してください。
フレームワークに依存しないロジック(ヘッドレス)を求めたり、Vue や Svelte などの他フレームワークとの共用を想定する場合に最適です。TanStack ecosystem との親和性が高く、カスタマイズ性を重視するプロジェクトに向いています。
レガシープロジェクトのメンテナンスを行う場合を除き、新規プロジェクトでの使用は推奨しません。メンテナンスが停滞しており、現代的な React のパターン(Hooks など)に準拠していないため、代替ライブラリへの移行を検討すべきです。
既存のレガシーコードベースで既に導入されており、移行コストが大きい場合を除き、新規プロジェクトでは避けるべきです。react-window によって置き換えられた経緯があり、バンドルサイズや API の複雑さで劣ります。
React 環境で標準的な仮想化リストを実装する際のファーストチョイスです。Brian Vaughn によって維持されており、API がシンプルで軽量、かつパフォーマンスが優れています。ほとんどのユースケースでバランスが最も取れています。
react-window を使用していて、さらに無限ローディング(スクロールに応じたデータ取得)機能が必要な場合に選択します。単体ではなく react-window と組み合わせて使用する専用コンポーネントです。
A versatile infinite scroll React component.
bower install react-list
# or
npm install react-list
ReactList depends on React.
Check out the example page and the the example page source for examples of different configurations.
Here's another simple example to get you started.
import loadAccount from 'my-account-loader';
import React from 'react';
import ReactList from 'react-list';
class MyComponent extends React.Component {
state = {
accounts: []
};
componentWillMount() {
loadAccounts(::this.handleAccounts);
}
handleAccounts(accounts) {
this.setState({accounts});
}
renderItem(index, key) {
return <div key={key}>{this.state.accounts[index].name}</div>;
}
render() {
return (
<div>
<h1>Accounts</h1>
<div style={{overflow: 'auto', maxHeight: 400}}>
<ReactList
itemRenderer={::this.renderItem}
length={this.state.accounts.length}
type='uniform'
/>
</div>
</div>
);
}
}
y)The axis that this list scrolls on.
An index to scroll to after mounting.
A function that receives an index and a key and returns the content to be rendered for the item at that index.
A function that receives the rendered items and a ref. By default this element
is just a <div>. Generally it only needs to be overridden for use in a
<table> or other special case. NOTE: You must set ref={ref} on the component
that contains the items so the correct item sizing calculations can be made.
A function that receives an item index and the cached known item sizes and
returns an estimated size (height for y-axis lists and width for x-axis lists)
of that item at that index. This prop is only used when the prop type is set
to variable and itemSizeGetter is not defined. Use this property when you
can't know the exact size of an item before rendering it, but want it to take up
space in the list regardless.
A function that receives an item index and returns the size (height for y-axis
lists and width for x-axis lists) of that item at that index. This prop is only
used when the prop type is set to variable.
0)The number of items in the list.
1)The minimum number of items to render at any given time. This can be used to render some amount of items initially when rendering HTML on the server.
10)The number of items to batch up for new renders. Does not apply to 'uniform'
lists as the optimal number of items is calculated automatically.
A function that returns a DOM Element or Window that will be treated as the scrolling container for the list. In most cases this does not need to be set for the list to work as intended. It is exposed as a prop for more complicated uses where the scrolling container may not initially have an overflow property that enables scrolling.
A function that returns the size of the scrollParent's viewport. Provide this prop if you can efficiently determine your scrollParent's viewport size as it can improve performance.
100)The number of pixels to buffer at the beginning and end of the rendered list items.
simple, variable, or uniform, defaults to simple)simple This type is...simple. It will not cache item sizes or remove items
that are above the viewport. This type is sufficient for many cases when the
only requirement is incremental rendering when scrolling.
variable This type is preferred when the sizes of the items in the list
vary. Supply the itemSizeGetter when possible so the entire length of the
list can be established beforehand. Otherwise, the item sizes will be cached
as they are rendered so that items that are above the viewport can be removed as
the list is scrolled.
uniform This type is preferred when you can guarantee all of your
elements will be the same size. The advantage here is that the size of the
entire list can be calculated ahead of time and only enough items to fill the
viewport ever need to be drawn. The size of the first item will be used to
infer the size of every other item. Multiple items per row are also supported
with this type.
false)Set to true if the item size will never change (as a result of responsive
layout changing or otherwise). This prop is only used when the prop type is
set to uniform. This is an opt-in optimization that will cause the very first
element's size to be used for all elements for the duration of the component's
life.
false)A boolean to determine whether the translate3d CSS property should be used for
positioning instead of the default translate. This can help performance on
mobile devices, but is supported by fewer browsers.
Put the element at index at the top of the viewport. Note that if you aren't
using type='uniform' or an itemSizeGetter, you will only be able to scroll
to an element that has already been rendered.
Scroll the viewport so that the element at index is visible, but not
necessarily at the top. The scrollTo note above also applies to this method.
[firstIndex, lastIndex]Return the indices of the first and last items that are at all visible in the viewport.
This happens when specifying the uniform type without actually providing
uniform size elements. The component attempts to draw only the minimum necessary
elements at one time and that minimum element calculation is based off the first
element in the list. When the first element does not match the other elements,
the calculation will be wrong and the component will never be able to fully
resolve the ideal necessary elements.
The calculations to figure out element positioning and size get significantly more complicated with margins, so they are not supported. Use a transparent border or padding or an element with nested elements to achieve the desired spacing.
If you need an onScroll handler, just add the handler to the div wrapping your ReactList component:
<div style={{height: 300, overflow: 'auto'}} onScroll={this.handleScroll}>
<ReactList ... />
</div>
open docs/index.html
make