はじめに:リストのパフォーマンス、甘く見てませんか?
React Nativeでアプリを作っていると、リスト表示って本当にあらゆる場面で出てきますよね。SNSのフィード、商品カタログ、チャット画面、設定メニュー——数え上げたらキリがないです。
で、ここが厄介なポイントなんですが、大量のデータを扱うリストのパフォーマンスは、アプリ全体のユーザー体験を左右します。正直、リストがカクついたらどんなに他の部分が良くても台無しです。
2026年現在、React Nativeのリストコンポーネントには主に3つの選択肢があります。標準のFlatList、Shopifyが開発したFlashList、そして比較的新しく登場したLegend Listです。この記事では、それぞれの仕組みや特徴、パフォーマンスを実際のコード例を交えながら比較していきます。
では、さっそく見ていきましょう。
FlatList:React Native標準のリストコンポーネント
FlatListの基本的な仕組み
FlatListはReact Nativeに標準搭載されている仮想化リストコンポーネントです。内部的にはVirtualizedListをベースにしていて、「仮想化(Virtualization)」という技術で画面に表示されているアイテムとそのバッファ領域だけをレンダリングします。
まずは基本的な使い方から。
import React from 'react';
import { FlatList, Text, View, StyleSheet } from 'react-native';
const DATA = Array.from({ length: 10000 }, (_, i) => ({
id: String(i),
title: `アイテム ${i + 1}`,
}));
const App = () => {
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
/>
);
};
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
title: {
fontSize: 16,
},
});
export default App;
FlatListの限界
FlatListは小〜中規模のリストなら問題ないんですが、以下のような状況ではパフォーマンスの問題がはっきりと出てきます。
- 大量データ(1,000件以上):スクロール中にフレームドロップが起きやすい
- 複雑なアイテムレイアウト:画像やネストされたコンポーネントを含むアイテムは、マウント/アンマウントのコストが重い
- ローエンドのAndroidデバイス:JSスレッドのCPU使用率が90%超になって、スクロールがガタガタになることも
- 空白セルの問題:高速スクロールすると、アイテムが描画される前に空白が見えてしまう
ある検証では、FlatListのJSスレッドFPSが平均9.28まで落ちた事例が報告されています。ユーザーは60FPSのスムーズなスクロールを期待しているわけで、この数値はかなり深刻ですよね。
FlatListのパフォーマンスチューニングProps
FlatListにはパフォーマンスを調整するためのPropsがいくつかあります。知っておくと便利です。
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
// 初期レンダリング数(デフォルト: 10)
initialNumToRender={10}
// バッチあたりの最大レンダリング数(デフォルト: 10)
maxToRenderPerBatch={5}
// ウィンドウサイズ(表示領域の倍数)
windowSize={5}
// 表示領域外のアイテムをクリップ
removeClippedSubviews={true}
// アイテムサイズが既知の場合にレイアウト計算をスキップ
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
これらを適切に設定すればある程度改善できますが、FlatListの根本的な設計——アイテムのマウントとアンマウントを繰り返す仕組み——自体は変えられません。ここが限界です。
FlashList:セルリサイクリングで劇的に高速化
FlashListの革新的なアプローチ
FlashListは、Shopifyのエンジニアリングチームが開発した高パフォーマンスリストコンポーネントです。
FlatListとの最大の違いは、セルリサイクリング(Cell Recycling)という戦略にあります。FlatListが画面外のアイテムを「破棄して再作成」するのに対し、FlashListはコンポーネントインスタンスの固定プールをメモリに保持して、スクロールで画面外に出たアイテムを新しいデータで再利用します。
つまり、コストの高いマウント/アンマウントのサイクルがなくなるわけです。この違いがパフォーマンスに与える影響は想像以上に大きいですよ。
FlashListのインストールと基本的な使い方
# インストール
npx expo install @shopify/flash-list
# または npm/yarn の場合
npm install @shopify/flash-list
yarn add @shopify/flash-list
嬉しいことに、FlashListはFlatListのドロップイン代替品として設計されています。最小限のコード変更で移行できるので、試すハードルがかなり低いです。
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
const DATA = Array.from({ length: 10000 }, (_, i) => ({
id: String(i),
title: `アイテム ${i + 1}`,
type: i % 3 === 0 ? 'featured' : 'normal',
}));
const App = () => {
const renderItem = ({ item }) => (
<View style={[
styles.item,
item.type === 'featured' && styles.featured
]}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlashList
data={DATA}
renderItem={renderItem}
estimatedItemSize={60}
getItemType={(item) => item.type}
/>
);
};
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
featured: {
backgroundColor: '#f0f8ff',
},
title: {
fontSize: 16,
},
});
export default App;
FlashListの重要なProps
FlashListのパフォーマンスを最大限引き出すために、押さえておきたいPropsを紹介します。
- estimatedItemSize:アイテムの推定サイズ(ピクセル単位)。レンダリング前のレイアウト最適化に使われます。全アイテムが同じサイズなら正確な値を、異なる場合は平均値か中央値を設定しましょう。
- getItemType:異なるタイプのアイテムを指定することで、リサイクルプールを効率的に管理できます。レイアウトが異なるアイテムが混在してる場合に特に効果的。
- overrideItemLayout:特定のアイテムのサイズやカラムスパンをオーバーライドできます。グリッドで特定のアイテムを複数カラムにまたがらせたいときに便利です。
FlashList v2の新機能(新アーキテクチャ対応)
FlashList v2はReact Nativeの新アーキテクチャ(Fabric + JSI)向けにゼロから再構築されたバージョンです。かなりの改善が入っています。
- サイズ推定が不要に:v1で必須だった
estimatedItemSizeが不要に。アイテムサイズを自動処理してくれます - マソンリーレイアウト:Pinterestスタイルのレイアウトをネイティブサポート
- maintainVisibleContentPosition:アイテム追加時にコンテンツがずれるのを自動補正(デフォルトで有効)
- JSのみのソリューション:ネイティブモジュール不要で、より軽量になりました
// FlashList v2 マソンリーレイアウトの例
import { MasonryFlashList } from '@shopify/flash-list';
const MasonryExample = () => (
<MasonryFlashList
data={imageData}
numColumns={2}
renderItem={({ item }) => (
<View style={{ height: item.height }}>
<Image source={{ uri: item.uri }} style={{ flex: 1 }} />
</View>
)}
estimatedItemSize={150}
/>
);
注意:FlashList v2.xは新アーキテクチャ専用です。旧アーキテクチャのプロジェクトではv1.xを使いましょう。
Legend List:100% JSで書かれた次世代リスト
Legend Listとは
Legend Listは、LegendAppチームが開発した高パフォーマンスリストコンポーネントです。2026年にバージョン1.0がリリースされ、「最速のReact Nativeリストライブラリ」を謳っています。
個人的に一番インパクトがあるのは、100% TypeScriptで書かれていてネイティブ依存が一切ないという点です。これ、地味にすごくないですか?
Legend Listの特徴
- 動的アイテムサイズのネイティブサポート:高さが異なるアイテムをパフォーマンス低下なしに処理。estimatedItemSizeの設定すら不要です
- オプショナルなリサイクリング:
recycleItemsプロパティでリサイクルの有効/無効を切り替え可能。内部状態を持つ複雑なアイテムにも柔軟に対応できます - チャットUI向け機能:
maintainScrollAtEndとalignItemsAtEndのおかげで、チャットアプリの実装がかなり楽になります - 双方向無限スクロール:上下両方向への無限スクロールをネイティブにサポート
- 新旧アーキテクチャの両方に対応:FlashList v2と違って、旧アーキテクチャでも問題なく動作します
Legend Listのインストールと使い方
# インストール
npm install @legendapp/list
# または
yarn add @legendapp/list
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { LegendList } from '@legendapp/list';
const DATA = Array.from({ length: 10000 }, (_, i) => ({
id: String(i),
title: `アイテム ${i + 1}`,
description: `これはアイテム ${i + 1} の説明文です。`,
}));
const App = () => {
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>
);
return (
<LegendList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
recycleItems
maintainScrollAtEnd={false}
/>
);
};
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
title: {
fontSize: 16,
fontWeight: 'bold',
},
description: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
});
export default App;
チャットUIの実装例
Legend Listが特に力を発揮するのがチャットUIです。
これまでReact Nativeでチャットリストを実装するときは、invertedプロパティによるリスト反転ハックが定番でした(あれ、正直あまり気持ちのいい実装じゃなかったですよね)。Legend Listではそれが不要になります。
import { LegendList } from '@legendapp/list';
const ChatScreen = ({ messages }) => {
return (
<LegendList
data={messages}
renderItem={({ item }) => (
<View style={[
styles.messageBubble,
item.isOwn ? styles.ownMessage : styles.otherMessage,
]}>
<Text>{item.text}</Text>
<Text style={styles.timestamp}>{item.time}</Text>
</View>
)}
keyExtractor={(item) => item.id}
alignItemsAtEnd
maintainScrollAtEnd
recycleItems
/>
);
};
3つのリストコンポーネント徹底比較
アーキテクチャの違い
まずは設計面での違いを整理してみましょう。
| 特徴 | FlatList | FlashList v2 | Legend List |
|---|---|---|---|
| レンダリング戦略 | 仮想化(マウント/アンマウント) | セルリサイクリング | 仮想化 + オプショナルリサイクリング |
| ネイティブ依存 | なし(標準搭載) | なし(v2からJS only) | なし(100% TypeScript) |
| 新アーキテクチャ対応 | 対応 | v2は新アーキテクチャ専用 | 新旧両対応 |
| 動的アイテムサイズ | getItemLayoutで対応 | v2で自動対応 | ネイティブ対応 |
| マソンリーレイアウト | 非対応 | v2で対応 | 非対応 |
| チャットUI機能 | invertedのみ | 基本的な対応 | 専用Props(maintainScrollAtEnd等) |
| リサイクリング制御 | 不可 | 常にリサイクル | recycleItemsで切替可能 |
パフォーマンス比較
次に気になるパフォーマンスの数値です。
| 指標 | FlatList | FlashList | Legend List |
|---|---|---|---|
| 10,000件のスクロールFPS | 約9〜30 FPS | 約55〜60 FPS | 約55〜60 FPS |
| 初期レンダリング速度 | 基準 | FlatListの約5〜10倍高速 | FlatListより高速 |
| メモリ使用量 | 高い | 低い(リサイクルによる) | 低い |
| JSスレッドCPU使用率 | 高い(90%以上の事例あり) | 低い(10%以下に改善) | 低い |
| 空白セルの発生 | 頻繁 | ほぼなし | ほぼなし |
FlashListとLegend ListのFPS差はほとんどありません。どちらも60FPSに近い数値を安定して出せます。FlatListとの差は歴然ですね。
FlatListからの移行ガイド
FlashListへの移行(3ステップ)
FlashListはドロップイン代替品なので、移行はとてもシンプルです。実際にやってみると「え、これだけ?」ってなると思います。
// ステップ1: インポートの変更
- import { FlatList } from 'react-native';
+ import { FlashList } from '@shopify/flash-list';
// ステップ2: コンポーネント名の変更 + estimatedItemSize の追加
- <FlatList
+ <FlashList
data={data}
renderItem={renderItem}
- keyExtractor={(item) => item.id}
+ estimatedItemSize={60}
/>
// ステップ3: getItemType の追加(異なるレイアウトがある場合)
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={60}
+ getItemType={(item) => item.type}
/>
移行時の注意点:
- アイテムコンポーネントに
keypropを設定しないでください。keyを使うとFlashListがビューをリサイクルできなくなって、パフォーマンスの利点が消えます React.memoでアイテムコンポーネントをラップするのがおすすめです- FlashListはラップする親Viewにサイズが必要です。
flex: 1を設定するか、明示的な高さを指定してください
Legend Listへの移行
Legend Listへの移行も同じくらい簡単です。FlashListからの乗り換えもスムーズにできます。
// FlatList からの移行
- import { FlatList } from 'react-native';
+ import { LegendList } from '@legendapp/list';
- <FlatList
+ <LegendList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
+ recycleItems
/>
// FlashList からの移行
- import { FlashList } from '@shopify/flash-list';
+ import { LegendList } from '@legendapp/list';
- <FlashList
+ <LegendList
data={data}
renderItem={renderItem}
- estimatedItemSize={60}
+ recycleItems
/>
実践的な最適化テクニック
1. React.memoで不要な再レンダリングを防ぐ
どのリストコンポーネントを使う場合でも、React.memoによるアイテムのメモ化は基本中の基本です。これをやるかやらないかで体感がかなり変わります。
import React, { memo, useCallback } from 'react';
import { Text, View, Pressable, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
// アイテムコンポーネントをmemo化
const ListItem = memo(({ item, onPress }) => (
<Pressable onPress={() => onPress(item.id)}>
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subtitle}>{item.subtitle}</Text>
</View>
</Pressable>
));
const OptimizedList = ({ data }) => {
// コールバックもuseCallbackでメモ化
const handlePress = useCallback((id) => {
console.log('Pressed:', id);
}, []);
const renderItem = useCallback(({ item }) => (
<ListItem item={item} onPress={handlePress} />
), [handlePress]);
return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={80}
/>
);
};
2. 画像の最適化
リスト内に画像がある場合、読み込みと描画のパフォーマンスは無視できません。expo-imageを使うとキャッシュやプレースホルダーの管理がぐっと楽になります。
import { Image } from 'expo-image';
const ListItemWithImage = memo(({ item }) => (
<View style={styles.item}>
<Image
source={{ uri: item.imageUrl }}
style={styles.thumbnail}
placeholder={{ blurhash: item.blurhash }}
contentFit="cover"
transition={200}
recyclingKey={item.id}
/>
<View style={styles.textContainer}>
<Text style={styles.title}>{item.title}</Text>
</View>
</View>
));
3. 無限スクロール(ページネーション)の実装
大量のデータを一気に読み込むのではなく、ページネーションで段階的に取得するのがセオリーです。初期表示の速度とメモリの両方に効きます。
import React, { useState, useCallback } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { FlashList } from '@shopify/flash-list';
const PAGE_SIZE = 20;
const InfiniteScrollList = () => {
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const loadMore = useCallback(async () => {
if (loading) return;
setLoading(true);
const nextPage = page + 1;
const newItems = await fetchItems(nextPage, PAGE_SIZE);
setData((prev) => [...prev, ...newItems]);
setPage(nextPage);
setLoading(false);
}, [loading, page]);
const renderFooter = useCallback(() => {
if (!loading) return null;
return (
<View style={{ padding: 16, alignItems: 'center' }}>
<ActivityIndicator size="small" />
</View>
);
}, [loading]);
return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={80}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={renderFooter}
/>
);
};
4. セクション付きリストの実装
FlashListでセクションヘッダー付きのリストを実装するなら、getItemTypeを活用するのがポイントです。リサイクリングも効率的に行えます。
import { FlashList } from '@shopify/flash-list';
// セクションとアイテムを統合したフラットなデータ構造
const flattenedData = [
{ type: 'header', title: 'カテゴリA' },
{ type: 'item', id: '1', name: 'アイテム1' },
{ type: 'item', id: '2', name: 'アイテム2' },
{ type: 'header', title: 'カテゴリB' },
{ type: 'item', id: '3', name: 'アイテム3' },
];
const SectionList = () => (
<FlashList
data={flattenedData}
renderItem={({ item }) => {
if (item.type === 'header') {
return (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{item.title}</Text>
</View>
);
}
return (
<View style={styles.item}>
<Text>{item.name}</Text>
</View>
);
}}
getItemType={(item) => item.type}
estimatedItemSize={50}
stickyHeaderIndices={
flattenedData
.map((item, index) => (item.type === 'header' ? index : null))
.filter((index) => index !== null)
}
/>
);
ユースケース別のおすすめリストコンポーネント
結局どれを使えばいいの?という方のために、ユースケース別にまとめました。
| ユースケース | おすすめ | 理由 |
|---|---|---|
| 設定画面(〜50件) | FlatList | シンプルで十分。追加ライブラリ不要 |
| 商品カタログ(100〜1,000件) | FlashList | セルリサイクリングで安定した60FPS |
| SNSフィード(無限スクロール) | FlashList / Legend List | 大量データの高速レンダリング |
| チャットUI | Legend List | 専用のチャット機能が充実 |
| 画像グリッド(Pinterest風) | FlashList v2 | マソンリーレイアウト対応 |
| 動的高さのアイテム | Legend List | estimatedItemSize不要で自然にサポート |
| 旧アーキテクチャのプロジェクト | FlashList v1 / Legend List | FlashList v2は新アーキテクチャ専用 |
よくある質問(FAQ)
FlatListからFlashListに移行するとアプリが壊れませんか?
基本的には大丈夫です。FlashListはFlatListのドロップイン代替品として設計されているので、ほとんどの場合はコンポーネント名の変更とestimatedItemSizeの追加だけで移行できます。ただし、アイテムコンポーネントにkey propを直接設定している場合は削除が必要です。念のため、移行前にテスト環境で動作確認しておくのをおすすめします。
FlashList v2とLegend Listのどちらを選ぶべきですか?
プロジェクトの要件次第です。新アーキテクチャを使っていてマソンリーレイアウトが必要ならFlashList v2がいいでしょう。チャットUIを作りたい場合や、旧アーキテクチャのプロジェクトならLegend Listのほうが向いています。パフォーマンス自体はどちらもしっかり高いので、必要な機能ベースで選んでOKです。
estimatedItemSizeにはどんな値を設定すればよいですか?
FlashList v1で必要なestimatedItemSizeには、リストアイテムの平均的な高さ(横スクロールなら幅)をピクセル単位で設定します。全アイテムが同じサイズならその正確な値を、サイズが異なるなら中央値がベストです。ちなみにFlashList v2とLegend Listではこのプロパティ自体が不要になっています。
新アーキテクチャに移行してない場合はどうすれば?
旧アーキテクチャのままなら、FlashList v1.xかLegend Listを使いましょう。FlashList v2.xは新アーキテクチャ(Fabric + JSI)専用なので旧環境では動きません。Legend Listは新旧どちらにも対応しているので、将来的に新アーキテクチャへの移行を考えている場合にも安心です。
リストのパフォーマンスはどうやって計測する?
React NativeのPerformanceモニターでJSスレッドとUIスレッドのFPSをリアルタイム監視できます。開発メニューから「Show Perf Monitor」を有効にしてください。FlashListには組み込みのパフォーマンス警告もあって、estimatedItemSizeの値が大きくずれていればコンソールに教えてくれます。本番環境の計測にはFlipperやreact-native-performanceなどのライブラリがおすすめです。