【React Native】FlatList viewabilityConfigの代替手法とパフォーマンス最適化
FlatList#viewabilityConfigとは?
FlatList
は、React Nativeで効率的に長いリストを表示するためのコンポーネントです。画面に表示されるアイテムだけをレンダリングすることでパフォーマンスを最適化する「仮想スクロール」の仕組みを持っています。
viewabilityConfig
は、このFlatList
のアイテムが「表示されている(viewable)」と判断される基準を細かく設定するためのプロパティです。具体的には、あるアイテムが画面のどの程度表示されたら「viewable」とみなすか、どれくらいの時間表示され続けたらコールバックをトリガーするかなどを定義できます。
この設定は、主にonViewableItemsChanged
というコールバックプロパティと組み合わせて使用されます。onViewableItemsChanged
は、viewabilityConfig
で定義された条件を満たすアイテムの表示状態が変化したときに呼び出される関数です。例えば、ユーザーがスクロールして動画のアイテムが画面に表示されたら自動再生を開始する、といった動作を実装する際に非常に役立ちます。
viewabilityConfig
で設定できる主なプロパティ
viewabilityConfig
はオブジェクトを受け取り、以下のプロパティを設定できます。
-
waitForInteraction?: boolean
true
に設定すると、ユーザーがリストをスクロールするなどの操作を行うまで、onViewableItemsChanged
コールバックが発火しません。- 通常は
false
にして、初回のレンダリング時やスクロール時に自動的に検出されるようにすることが多いです。ただし、一部の環境で初回の発火に問題がある場合、この設定やFlatList
のonLayout
とrecordInteraction()
メソッドを組み合わせることで解決できることがあります。
-
itemVisiblePercentThreshold?: number
viewAreaCoveragePercentThreshold
と似ていますが、ビューポートがカバーする割合ではなく、アイテム自体の何パーセントが画面に表示されているかを基準にします。- 例えば、
50
を設定すると、アイテムの50%以上が画面に表示されていれば「viewable」とみなされます。
-
viewAreaCoveragePercentThreshold?: number
- 部分的に隠れているアイテムが「viewable」とみなされるために、ビューポート(画面の表示領域)の何パーセントをカバーする必要があるか(0〜100)。
- 完全に画面に表示されているアイテムは常に「viewable」とみなされます。
0
を設定すると、1ピクセルでも画面に表示されれば「viewable」とみなされます。100
を設定すると、アイテムが完全に画面に表示されているか、ビューポート全体を覆う必要がある場合にのみ「viewable」とみなされます。
-
- アイテムが「viewable」と判断されるために、物理的に画面に表示され続けなければならない最小時間(ミリ秒)です。
- この値を大きく設定すると、スクロール中に一時的に画面に入っただけのアイテムは「viewable」とみなされにくくなります。動画の自動再生など、ユーザーがそのアイテムを「実際に見た」と判断したい場合に役立ちます。
import React, { useState, useRef, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';
const DATA = Array.from({ length: 50 }, (_, i) => ({ id: String(i), title: `アイテム ${i + 1}` }));
const App = () => {
const [viewableItems, setViewableItems] = useState([]);
// viewabilityConfigの設定
const viewabilityConfig = useRef({
minimumViewTime: 500, // 500ミリ秒以上表示されたらviewable
itemVisiblePercentThreshold: 50, // アイテムの50%以上が画面に表示されたらviewable
waitForInteraction: false, // ユーザー操作を待たずに検出
}).current;
// onViewableItemsChanged コールバック
const onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
console.log("Viewable Items:", viewableItems.map(item => item.item.title));
console.log("Changed Items:", changed.map(item => item.item.title));
setViewableItems(viewableItems.map(item => item.item.title));
}, []);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>現在表示されているアイテム:</Text>
{viewableItems.length > 0 ? (
<Text>{viewableItems.join(', ')}</Text>
) : (
<Text>スクロールしてください</Text>
)}
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
style={styles.flatList}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
backgroundColor: '#f5f5f5',
},
header: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
marginLeft: 20,
},
item: {
backgroundColor: '#fff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
title: {
fontSize: 20,
},
flatList: {
marginTop: 20,
},
});
export default App;
FlatList#viewabilityConfig におけるよくあるエラーとトラブルシューティング
onViewableItemsChangedが期待通りに発火しない/一度しか発火しない
これは最もよく遭遇する問題の一つです。
- 原因5: 初回レンダリング時に
onViewableItemsChanged
が発火しない- 一部の環境や特定の条件(特にAndroid)で、初期ロード時に
onViewableItemsChanged
が発火しないことがあります。 - トラブルシューティング
FlatList
のonLayout
プロパティでrecordInteraction()
を呼び出すことで、明示的にインタラクションを記録し、Viewabilityの計算をトリガーすることができます。
import React, { useRef, useCallback } from 'react'; import { FlatList, View, Text } from 'react-native'; const MyList = () => { const flatListRef = useRef(null); const onViewableItemsChanged = useCallback(({ viewableItems }) => { console.log(viewableItems); }, []); return ( <FlatList ref={flatListRef} data={[]} renderItem={() => <Text>Item</Text>} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} onLayout={() => { // 初回レンダリング時にviewabilityをトリガー flatListRef.current?.recordInteraction(); }} /> ); };
- あるいは、
initialScrollIndex={0.01}
のような小さな値で強制的に初期スクロールを発生させることで、イベントをトリガーできるという報告もあります(これはあまり推奨される解決策ではありません)。
- 一部の環境や特定の条件(特にAndroid)で、初期ロード時に
- 原因4:
keyExtractor
が適切に設定されていないFlatList
はアイテムの識別と最適化のためにkeyExtractor
を必要とします。一意なキーが提供されない場合、仮想化のパフォーマンス問題や、onViewableItemsChanged
が正しく動作しない原因となることがあります。- トラブルシューティング
keyExtractor
が各アイテムに対して一意で安定した文字列を返すことを確認してください。通常はデータアイテムのIDプロパティを使用します。
- 原因3:
FlatList
が他のスクロール可能なコンポーネント(例:ScrollView
)の内部にあるFlatList
を別のScrollView
の中にネストすると、FlatList
の仮想化機能が正しく動作せず、すべてのアイテムが一度にレンダリングされてしまい、onViewableItemsChanged
が期待通りに動作しなくなることがあります。これはFlatList
のViewability検出メカニズムが、親のスクロールコンテナのサイズを正しく認識できなくなるためです。- トラブルシューティング
- 基本的に
FlatList
をScrollView
の中にネストすることは避けるべきです。リストの先頭や末尾にヘッダーやフッターを追加したい場合は、FlatList
のListHeaderComponent
やListFooterComponent
プロパティを使用することを検討してください。
- 基本的に
- 原因2:
viewabilityConfig
の設定が厳しすぎるminimumViewTime
が長すぎたり、itemVisiblePercentThreshold
やviewAreaCoveragePercentThreshold
が高すぎたりすると、アイテムが「viewable」と判断される条件が厳しくなり、なかなかイベントが発火しないことがあります。- トラブルシューティング
viewabilityConfig
の値を調整してみてください。特に開発中は、minimumViewTime: 0
やitemVisiblePercentThreshold: 1
など、非常に緩い設定で試してみて、イベントが発火するかどうかを確認すると良いでしょう。waitForInteraction: true
に設定している場合、ユーザーがリストをスクロールするなど、何らかの操作を行うまでイベントが発火しません。初期表示時に発火させたい場合はfalse
に設定してください。
- 原因1:
onViewableItemsChanged
関数の不安定性(再生成)- Reactの関数コンポーネント内で
onViewableItemsChanged
をインラインで定義したり、依存配列を適切に設定せずにuseCallback
を使用したりすると、コンポーネントが再レンダリングされるたびにこの関数が再生成されてしまいます。FlatList
は、onViewableItemsChanged
関数が「その場で変更される」ことをサポートしていません。 - トラブルシューティング
- 関数コンポーネントを使用している場合、
onViewableItemsChanged
コールバックをReact.useCallback
でラップし、依存配列を空[]
にするか、本当に必要な依存関係のみを含めるようにしてください。これにより、関数が再レンダリング時に再生成されなくなります。 - もしコールバック内でstateの値にアクセスする必要があるが、依存配列にそのstateを含めたくない(含めると関数が再生成されてしまう)場合は、
useRef
を使ってstateの最新の値を参照するようにすることで、useCallback
の依存配列を空に保つことができます。
const myState = useRef(initialState); // ... state updates ... myState.current = newState; const onViewableItemsChanged = useCallback(({ viewableItems }) => { // myState.current の値を使って処理 console.log(myState.current); }, []); // 依存配列は空
- 関数コンポーネントを使用している場合、
- Reactの関数コンポーネント内で
onViewableItemsChangedが頻繁に、または不必要に発火する
- 原因2:
onViewableItemsChanged
内で重い処理を実行しているonViewableItemsChanged
はスクロール中に頻繁に呼び出される可能性があるため、その内部でsetStateを頻繁に呼び出したり、時間のかかる計算を行ったりすると、JSスレッドがブロックされ、スクロールのパフォーマンスが低下する可能性があります。- トラブルシューティング
- コールバック内で実行する処理を最小限に抑えてください。
- setStateの更新をデバウンス(debounce)するか、必要な状態更新のみを行うようにロジックを最適化してください。
- パフォーマンスが気になる場合は、
InteractionManager.runAfterInteractions()
を使用して、UIスレッドのアイドル時に処理を実行するように検討してください。 - 複雑な計算が必要な場合は、Web Worker(React Nativeでは専用のライブラリが必要)などを利用してバックグラウンドで処理を行うことも視野に入れます。
- 原因1:
viewabilityConfig
の設定が緩すぎるminimumViewTime
が短すぎる、またはitemVisiblePercentThreshold
やviewAreaCoveragePercentThreshold
が低すぎると、少しでも画面に入っただけで頻繁にイベントが発火してしまい、パフォーマンスに影響を与えたり、意図しない挙動を引き起こしたりすることがあります。- トラブルシューティング
- アプリケーションの要件に合わせて、これらの値を調整してください。例えば、動画の自動再生であれば
minimumViewTime
を長めに設定し、スクロールの途中で一瞬見えただけでは再生しないようにするなど、具体的なユースケースを考慮して調整します。
- アプリケーションの要件に合わせて、これらの値を調整してください。例えば、動画の自動再生であれば
表示されたアイテムのデータが古い(Stale Closure)
- トラブルシューティング
- 前述の「
useRef
を使用してstateの最新の値を参照する」方法を検討してください。これが最も一般的な解決策です。
const visibleItemsRef = useRef([]); // 表示アイテムを保持するref const onViewableItemsChanged = useCallback(({ viewableItems }) => { // 最新のviewableItemsを使って何か処理 visibleItemsRef.current = viewableItems.map(item => item.item.id); // 必要に応じて、stateを更新してUIを再レンダリングする // setSomeState(prev => ...); }, []); // 依存配列は空
- あるいは、
useState
の更新関数(setState
の関数形式)を使って、常に最新のstateにアクセスするようにすることも可能です。
const [viewableItems, setViewableItems] = useState([]); const onViewableItemsChanged = useCallback(({ viewableItems }) => { setViewableItems(prevItems => { // prevItems は常に最新のstate const currentVisibleIds = viewableItems.map(item => item.item.id); // ... 何らかのロジック ... return currentVisibleIds; // 新しいstateを返す }); }, []);
- 前述の「
- 原因
onViewableItemsChanged
コールバックがuseCallback
でメモ化されている場合、依存配列に含めなかった外部のstateやpropsの値が、コールバックが生成された時点の古い値のままになってしまうことがあります。これはReactのクロージャー(Closure)の特性によるものです。
Androidでのパフォーマンス問題や予期せぬ挙動
- トラブルシューティング
removeClippedSubviews={true}
を試す(ただし、これにはバグがある場合や意図しない表示問題を引き起こす可能性もあるため注意が必要です)。windowSize
、initialNumToRender
、maxToRenderPerBatch
などのパフォーマンス関連のプロパティを調整して、レンダリングされるアイテムの数を制御します。getItemLayout
プロパティを使用して、各アイテムの固定高さをFlatListに伝えることで、レイアウト計算のオーバーヘッドを削減し、パフォーマンスを向上させることができます。renderItem
内で使用するコンポーネントがReact.memo
やPureComponent
で最適化されていることを確認します。- 画像を使用している場合は、
react-native-fast-image
のような高速な画像ライブラリの使用を検討します。
- 原因
Androidでは、iOSに比べて仮想化の挙動が異なる場合があり、特に複雑なレイアウトや画像が多いリストでパフォーマンスの問題が発生しやすいです。
- 公式ドキュメントとGitHub Issues
React Nativeの公式ドキュメントや、FlatList
に関連するGitHubのIssueを検索すると、同じ問題に直面している他の開発者の情報や、解決策が見つかることがあります。 - React DevTools / Flipper
これらを活用してコンポーネントの再レンダリング回数を監視したり、プロファイリングを行ったりすることで、パフォーマンスボトルネックを発見できます。 - 開発モードとプロダクションモードの挙動の違い
開発モードではReact Nativeのホットリロードや追加のチェックのためにパフォーマンスが低下したり、特定の挙動が異なったりすることがあります。最終的な挙動はプロダクションビルドで確認することが重要です。 - デバッグログの活用
onViewableItemsChanged
コールバック内でconsole.log
を使って、viewableItems
やchanged
の内容、イベントが発火している頻度などを確認します。
FlatList#viewabilityConfig のプログラミング例
例1: アイテムが画面に半分以上表示されたら検出する
この例では、アイテムの50%以上が画面に表示されたときにonViewableItemsChanged
が発火するように設定します。
import React, { useState, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';
const DATA = Array.from({ length: 30 }, (_, i) => ({
id: `item-${i}`,
title: `アイテム ${i + 1}`,
height: 100 + (i % 3) * 20, // 高さのバリエーションを持たせる
}));
const Example1 = () => {
const [visibleItemTitles, setVisibleItemTitles] = useState([]);
// viewabilityConfig の設定
const viewabilityConfig = {
// アイテムの50%以上が画面に表示されたらViewableと判断
itemVisiblePercentThreshold: 50,
// viewAreaCoveragePercentThreshold: 0, // これを使う場合はitemVisiblePercentThresholdと排他的に使うか、両方考慮される
minimumViewTime: 0, // すぐに検出
waitForInteraction: false, // ユーザー操作を待たない
};
// onViewableItemsChanged コールバック
const onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
const currentVisibleTitles = viewableItems.map(item => item.item.title);
console.log('--- Example 1 ---');
console.log('Viewable Items (50% threshold):', currentVisibleTitles);
// 状態を更新してUIに表示
setVisibleItemTitles(currentVisibleTitles);
}, []); // 依存配列は空でOK。状態更新はsetterを使うのでクロージャ問題なし
const renderItem = ({ item }) => (
<View style={[styles.item, { height: item.height }]}>
<Text style={styles.title}>{item.title}</Text>
<Text>高さ: {item.height}px</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>
アイテムが50%以上表示されたら検出:
</Text>
<Text style={styles.visibleStatus}>
現在表示中: {visibleItemTitles.length > 0 ? visibleItemTitles.join(', ') : 'なし'}
</Text>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
style={styles.flatList}
/>
</View>
);
};
// スタイルは最後にまとめて定義
例2: アイテムが画面に200ミリ秒以上表示され続けたら検出する
この例では、minimumViewTime
を使用して、アイテムが一定時間(200ミリ秒)画面に表示され続けた場合にのみ「viewable」と判断するようにします。これにより、高速なスクロール中に一瞬だけ見えたアイテムが検出されるのを防ぐことができます。
import React, { useState, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';
// DATAは例1と同じものを使用
const DATA = Array.from({ length: 30 }, (_, i) => ({
id: `item-${i}`,
title: `アイテム ${i + 1}`,
height: 100 + (i % 3) * 20,
}));
const Example2 = () => {
const [visibleItemTitles, setVisibleItemTitles] = useState([]);
// viewabilityConfig の設定
const viewabilityConfig = {
itemVisiblePercentThreshold: 1, // 1%でも表示されたらviewableの候補
// 200ミリ秒以上表示され続けたらViewableと判断
minimumViewTime: 200,
waitForInteraction: false,
};
// onViewableItemsChanged コールバック
const onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
const currentVisibleTitles = viewableItems.map(item => item.item.title);
console.log('--- Example 2 ---');
console.log('Viewable Items (minimumViewTime 200ms):', currentVisibleTitles);
setVisibleItemTitles(currentVisibleTitles);
}, []);
const renderItem = ({ item }) => (
<View style={[styles.item, { height: item.height }]}>
<Text style={styles.title}>{item.title}</Text>
<Text>高さ: {item.height}px</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>
200ms以上表示されたら検出:
</Text>
<Text style={styles.visibleStatus}>
現在表示中: {visibleItemTitles.length > 0 ? visibleItemTitles.join(', ') : 'なし'}
</Text>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
style={styles.flatList}
/>
</View>
);
};
例3: waitForInteraction
の使用とrecordInteraction
による初期トリガー
waitForInteraction: true
に設定すると、ユーザーがリストをスクロールするなどの操作を行うまでonViewableItemsChanged
は発火しません。しかし、初期表示時に発火させたい場合は、FlatList
のref
を使ってrecordInteraction()
を呼び出すことで、明示的にViewabilityの計算をトリガーできます。
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { FlatList, View, Text, StyleSheet, Button } from 'react-native';
// DATAは例1と同じものを使用
const DATA = Array.from({ length: 30 }, (_, i) => ({
id: `item-${i}`,
title: `アイテム ${i + 1}`,
height: 100 + (i % 3) * 20,
}));
const Example3 = () => {
const [visibleItemTitles, setVisibleItemTitles] = useState([]);
const flatListRef = useRef(null); // FlatListへの参照
// viewabilityConfig の設定
const viewabilityConfig = {
itemVisiblePercentThreshold: 50,
minimumViewTime: 0,
// ユーザーがスクロールするまでonViewableItemsChangedは発火しない
waitForInteraction: true,
};
const onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
const currentVisibleTitles = viewableItems.map(item => item.item.title);
console.log('--- Example 3 ---');
console.log('Viewable Items (waitForInteraction):', currentVisibleTitles);
setVisibleItemTitles(currentVisibleTitles);
}, []);
// コンポーネントがマウントされた後に recordInteraction を呼び出す
useEffect(() => {
// 開発環境のFast Refreshで複数回呼ばれる可能性があるので注意
console.log('Component mounted, calling recordInteraction()');
flatListRef.current?.recordInteraction();
}, []); // 空の依存配列でマウント時のみ実行
const renderItem = ({ item }) => (
<View style={[styles.item, { height: item.height }]}>
<Text style={styles.title}>{item.title}</Text>
<Text>高さ: {item.height}px</Text>
</View>
);
const handleRecordInteraction = () => {
console.log('Manual recordInteraction() called.');
flatListRef.current?.recordInteraction();
};
return (
<View style={styles.container}>
<Text style={styles.header}>
`waitForInteraction`と`recordInteraction`:
</Text>
<Text style={styles.visibleStatus}>
現在表示中: {visibleItemTitles.length > 0 ? visibleItemTitles.join(', ') : 'なし'}
</Text>
<Button title="手動でViewabilityをトリガー" onPress={handleRecordInteraction} />
<FlatList
ref={flatListRef} // refを設定
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
style={styles.flatList}
/>
</View>
);
};
スタイル定義 (すべての例で共通)
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
backgroundColor: '#f0f0f0',
},
header: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 5,
marginLeft: 15,
},
visibleStatus: {
fontSize: 14,
color: '#555',
marginBottom: 15,
marginLeft: 15,
},
flatList: {
flex: 1,
},
item: {
backgroundColor: '#fff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
});
// 各Exampleコンポーネントをエクスポートして、App.jsなどで切り替えて実行できます
// export default Example1;
// export default Example2;
// export default Example3;
// 例えば、App.jsで以下のようにインポートして試せます
/*
import React from 'react';
import Example1 from './Example1'; // or './Example2', './Example3'
const App = () => {
return <Example1 />;
};
export default App;
*/
itemVisiblePercentThreshold
: アイテムが画面にどの程度表示されたら「viewable」と見なすかを制御します。動画の自動再生など、アイテムが「きちんと見えている」と判断したい場合に役立ちます。minimumViewTime
: アイテムが「viewable」と判断されるために、画面に表示され続ける最小時間を制御します。高速スクロール時の不要な検出を防ぎ、リソースの無駄遣いを避けるのに役立ちます。waitForInteraction
とrecordInteraction()
: 初回ロード時や特定のイベント後にViewabilityの計算をトリガーしたいが、通常はユーザー操作を待つ設定にしたい場合に有効です。
ScrollViewのonScrollイベントとmeasure()メソッドを組み合わせる
最も基本的な代替手段は、ScrollView
のonScroll
イベントリスナーと、各アイテムコンポーネントのmeasure()
メソッド(またはmeasureInWindow()
、measureLayout()
)を組み合わせて、アイテムの画面上の位置を計算する方法です。
仕組み
- ScrollViewのonScroll
スクロールイベントが発生するたびに呼び出され、現在のスクロール位置(event.nativeEvent.contentOffset.y
など)を取得できます。 - 各アイテムのrefとonLayout
各アイテムコンポーネントにref
を設定し、そのonLayout
イベントでアイテム自体のレイアウト情報(幅、高さ、x/y座標)を取得します。 - measure()
スクロールイベントが発生した際に、各アイテムのref
を使ってmeasure()
メソッドを呼び出し、そのアイテムがビューポートのどこに位置しているかを計算します。measure((x, y, width, height, pageX, pageY) => { ... })
x, y
はビューポート内での相対位置、pageX, pageY
は画面全体での絶対位置です。
利点
ScrollView
は、要素が少量で、かつすべての要素を常にマウントしておきたい場合に適しています。- 非常に特定のViewabilityの定義が必要な場合に、独自の計算ロジックを実装できます。
FlatList
を使わないため、より低レベルでリストの表示を完全に制御できます。(ただし、FlatList
の仮想化によるパフォーマンス最適化は失われます。)
欠点
- 仮想化の欠如
FlatList
のような仮想化レンダリング(画面に表示されているアイテムのみをレンダリングする)が行われないため、メモリ使用量が増加し、UIが重くなる可能性があります。これにより、非常に長いリストには適していません。 - 複雑なロジック
Viewabilityの条件(例:アイテムの半分が表示されたら、一定時間表示されたら)を自分で実装する必要があり、FlatList#viewabilityConfig
に比べてロジックが複雑になります。 - パフォーマンス問題
特に長いリストの場合、onScroll
イベントが非常に頻繁に発火し、各アイテムのmeasure()
を呼び出すことでパフォーマンスが著しく低下する可能性があります。JavaScriptスレッドとUIスレッド間のブリッジの負荷が高くなります。
import React, { useRef, useState, useCallback } from 'react';
import { ScrollView, View, Text, StyleSheet, Dimensions } from 'react-native';
const { height: screenHeight } = Dimensions.get('window');
const DATA = Array.from({ length: 50 }, (_, i) => ({ id: `item-${i}`, title: `アイテム ${i + 1}`, height: 120 }));
const ManualViewability = () => {
const itemRefs = useRef({}); // 各アイテムのrefを保存
const scrollViewRef = useRef(null);
const [visibleItems, setVisibleItems] = useState([]);
// 各アイテムのレイアウト情報を保持する(オプション)
// 常に最新のレイアウトをmeasureするのが確実だが、パフォーマンスのためにキャッシュも考慮
const itemLayouts = useRef({});
const handleScroll = useCallback(() => {
scrollViewRef.current?.measureInWindow((x, y, width, height, pageX, pageY) => {
const scrollViewTop = pageY; // ScrollViewの画面上での位置
const currentlyVisible = [];
DATA.forEach(item => {
const itemRef = itemRefs.current[item.id];
if (itemRef) {
itemRef.measureInWindow((itemX, itemY, itemWidth, itemHeight, itemPageX, itemPageY) => {
// アイテムのビューポート内での相対位置
const itemRelativeY = itemPageY - scrollViewTop;
// アイテムがScrollViewの表示領域内にどれだけ入っているか(例:50%以上)
const overlap = Math.max(0, Math.min(itemRelativeY + itemHeight, height) - Math.max(itemRelativeY, 0));
const visiblePercentage = (overlap / itemHeight) * 100;
if (visiblePercentage >= 50) {
currentlyVisible.push(item.title);
}
});
}
});
// 実際にはデバウンスなどでsetStatesの呼び出しを最適化する
// このロジックはパフォーマンスを考慮して非常に慎重に記述する必要がある
// setVisibleItems(currentlyVisible); // 頻繁な更新は避けるべき
});
}, []);
const setItemRef = useCallback((itemId, element) => {
if (element) {
itemRefs.current[itemId] = element;
}
}, []);
const renderItem = ({ item }) => (
<View ref={el => setItemRef(item.id, el)} style={[styles.item, { height: item.height }]}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>手動Viewability検出 (ScrollView):</Text>
<Text style={styles.visibleStatus}>
// ここに表示中のアイテムリストを表示するロジックは省略(複雑になるため)
</Text>
<ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16} // イベント発火頻度を制御 (16ms = 60fps)
style={styles.flatList}
>
{DATA.map(item => renderItem({ item }))}
</ScrollView>
</View>
);
};
外部の高性能リストライブラリの使用
React NativeのFlatList
は標準的なリストコンポーネントですが、さらに高度なパフォーマンスや特定の機能が必要な場合、コミュニティ製のライブラリが代替手段として存在します。これらのライブラリは、viewabilityConfig
と同等かそれ以上のViewability検出機能を提供していることが多いです。
-
RecyclerListView
(Flipkart製)FlashList
と同様に、ビューの再利用に焦点を当てた高性能なリストコンポーネントです。- より低レベルなAPIを提供しており、
FlatList
よりも学習コストがかかりますが、その分柔軟性も高まります。 - Viewabilityの検出機能も提供しています。
利点
- 非常に高いパフォーマンスとメモリ効率。
- 柔軟性が高く、複雑なリストUIも構築可能。
欠点
FlatList
やFlashList
に比べてAPIが複雑で、学習コストが高い。- ドキュメントがやや不十分な場合がある。
-
FlashList
(Shopify製)FlatList
のドロップイン代替を目指して開発されており、非常に高いパフォーマンスを誇ります。- Recycling(ビューの再利用)というAndroidの
RecyclerView
に似た仕組みを採用しており、メモリ使用量とレンダリング速度が大幅に改善されています。 viewabilityConfig
とほぼ同じプロパティやonViewableItemsChanged
コールバックをサポートしており、FlatList
からの移行も比較的容易です。- 特にデータ数が非常に多いリストや、複雑なアイテムレイアウトを持つリストで威力を発揮します。
利点
FlatList
よりも優れたパフォーマンス。FlatList
とほとんど同じAPIを持ち、移行が簡単。viewabilityConfig
と同等の機能が提供されている。
欠点
- すべての
FlatList
のプロパティを完全にサポートしているわけではない場合がある。 - まだ比較的新しいライブラリであるため、予期せぬ挙動やバグに遭遇する可能性もゼロではない。
import React, { useState, useCallback } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { FlashList } from '@shopify/flash-list'; // FlashListをインポート const DATA = Array.from({ length: 1000 }, (_, i) => ({ id: `item-${i}`, title: `Flashアイテム ${i + 1}`, height: 100 + (i % 3) * 20, })); const FlashListViewability = () => { const [visibleItemTitles, setVisibleItemTitles] = useState([]); const viewabilityConfig = { itemVisiblePercentThreshold: 50, minimumViewTime: 200, }; const onViewableItemsChanged = useCallback(({ viewableItems }) => { const currentVisibleTitles = viewableItems.map(item => item.item.title); console.log('--- FlashList Example ---'); console.log('FlashList Viewable Items:', currentVisibleTitles); setVisibleItemTitles(currentVisibleTitles); }, []); const renderItem = useCallback(({ item }) => ( <View style={[styles.item, { height: item.height }]}> <Text style={styles.title}>{item.title}</Text> <Text>高さ: {item.height}px</Text> </View> ), []); // renderItemもuseCallbackでメモ化すると良い return ( <View style={styles.container}> <Text style={styles.header}>FlashListによるViewability検出:</Text> <Text style={styles.visibleStatus}> 現在表示中: {visibleItemTitles.length > 0 ? visibleItemTitles.join(', ') : 'なし'} </Text> <FlashList data={DATA} renderItem={renderItem} keyExtractor={item => item.id} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={viewabilityConfig} estimatedItemSize={120} // FlashListで非常に重要 style={styles.flatList} /> </View> ); };
IntersectionObserverのようなカスタムフック/ライブラリ(ウェブの概念を応用)
ウェブ開発では要素のViewabilityを検出する標準APIとしてIntersectionObserver
がありますが、React Nativeには直接的な同等のAPIは組み込まれていません。しかし、これを模倣したライブラリやカスタムフックを作成することで、特定のコンポーネントのViewabilityを個別に検出することも可能です。
-
react-native-intersection-observer
(コミュニティ製)- ウェブの
IntersectionObserver
APIにインスパイアされたライブラリで、FlatList
やScrollView
の子要素のViewabilityを個別に監視できます。 FlatList
のviewabilityConfig
がリスト全体のアイテムを監視するのに対し、この種のライブラリは個々のコンポーネントにアタッチしてその表示状態を監視するのに適しています。
利点
- コンポーネントレベルでViewabilityを監視できるため、特定の要素の表示・非表示に応じたアクション(例:アニメーションの開始/停止)に便利。
- Webの
IntersectionObserver
に慣れている開発者には理解しやすいAPI。
欠点
FlatList
の仮想化と組み合わせる場合、FlatList
がアイテムをアンマウント/マウントする際に、IntersectionObserver
のインスタンスも再作成される可能性がある。- リスト全体ではなく個々のアイテムにアタッチするため、アイテム数が非常に多い場合にオーバーヘッドが生じる可能性がある。
- 依存関係を追加する必要がある。
- ウェブの
コード例(概念的)
// 例えば、このようなカスタムフックを作成すると仮定(ライブラリとして提供されている場合が多い)
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { View, Text, Dimensions, findNodeHandle, ScrollView } from 'react-native';
// 簡略化されたIntersectionObserver風カスタムフックの概念
// 実際のライブラリはもっと複雑なロジックを持つ
const useInView = (ref, options = {}) => {
const [inView, setInView] = useState(false);
const scrollOffsetRef = useRef(0);
const checkTimerRef = useRef(null);
const handleScroll = useCallback((event) => {
scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
// デバウンスしてチェック頻度を抑える
if (checkTimerRef.current) clearTimeout(checkTimerRef.current);
checkTimerRef.current = setTimeout(() => {
if (ref.current) {
ref.current.measureLayout(
findNodeHandle(event.target), // ScrollViewのnative handle
(x, y, width, height) => {
const elementTop = y;
const elementBottom = y + height;
const viewportTop = scrollOffsetRef.current;
const viewportBottom = scrollOffsetRef.current + Dimensions.get('window').height;
const isIntersecting =
elementBottom > viewportTop && elementTop < viewportBottom;
// 例: 50%以上表示されたらinViewとみなす
const overlap = Math.max(0, Math.min(elementBottom, viewportBottom) - Math.max(elementTop, viewportTop));
const visiblePercentage = (overlap / height) * 100;
if (visiblePercentage >= (options.threshold || 1)) {
setInView(true);
} else {
setInView(false);
}
},
() => {} // error callback
);
}
}, 100); // 100msごとにチェック
}, [options.threshold]);
return { inView, handleScroll }; // ScrollViewにアタッチするhandleScrollを返す
};
// これをFlatListやScrollViewの子コンポーネント内で使用する
const ItemWithInView = ({ item, scrollViewRef }) => {
const itemRef = useRef(null);
const { inView } = useInView(itemRef, { threshold: 50 }); // このthresholdはuseInView内で使う
useEffect(() => {
if (inView) {
console.log(`${item.title} が表示されました!`);
// 例: 動画の自動再生、アニメーションの開始など
} else {
console.log(`${item.title} が非表示になりました。`);
}
}, [inView, item.title]);
return (
<View ref={itemRef} style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
{inView && <Text style={styles.inViewText}>[表示中]</Text>}
</View>
);
};
// スタイルは上記の例と同じ
// メインのFlatListまたはScrollViewコンポーネントでItemWithInViewを使用
// このアプローチでは、FlatListのonViewableItemsChangedを使う代わりに、
// 各アイテムが自身の表示状態を管理する(これはかなり重い処理になる可能性が高い)
// 実際には、カスタムフックがScrollView/FlatListのonScrollイベントを直接監視し、
// 全てのアイテムのrefから位置を計算して、まとめてViewabilityを判定するような構造になる
// 上記のuseInViewは、単純化された概念的なもので、実際の実装はもっと複雑でパフォーマンスを考慮する必要があります。
FlatList#viewabilityConfig
は、React NativeでリストのViewability検出を行うための最も推奨される方法です。ほとんどのユースケースではこれで十分であり、パフォーマンスも最適化されています。
代替手段を検討するのは、以下のような場合に限られるでしょう。
- リスト全体ではなく、特定の数個の独立したコンポーネントの画面表示状態を監視したい場合(この場合は
ScrollView
のonScroll
とmeasure
の組み合わせが検討されるが、前述のパフォーマンス問題に注意が必要)。 FlatList
のパフォーマンスがアプリケーションのボトルネックになっており、FlashList
やRecyclerListView
のようなより高性能なリストライブラリへの移行が必要な場合。- 極めて特殊なViewabilityの定義が必要で、
viewabilityConfig
のオプションではカバーできない場合。(この場合でもFlatList
のonViewableItemsChanged
の内部ロジックを独自にカスタマイズする方が良いことが多いです。)