【React Native】FlatListが重いときの処方箋。効果が大きい順に試す4つの最適化手順

エンジニアの記録
技術メモ・実体験

リストの件数が増えた途端、スクロールがカクつく・初期表示が固まる。React Nativeのリスト表示で必ず一度はぶつかる壁です。

この記事でわかること

FlatList が重くなる原因を切り分け、効く順に最適化していく手順をコードつきで書きます。「とりあえず全部盛り」ではなく、何がどう効くかを理解して直せるようにします。

PROFILE 20代ITエンジニア|React Native(Expo)× AWS Amplifyでアプリ開発中|Raw Ambition 運営
キラ
キラ
まず最初にやるべきは「ScrollViewでmapしてないか」の確認。これだけで直ることも多い。
Contents
  1. 結論:効く順に4つ直す
  2. 大前提:ScrollView+mapをやめる
  3. keyExtractorと行のmemo化
  4. getItemLayoutで描画を先読みさせる
  5. レンダリング件数のチューニング
  6. まとめ
結論:効く順に4つ直す

FlatListの最適化は順番が大事です。効果が大きい順に、①ScrollView+mapをFlatListにする ②行をmemo化する ③getItemLayoutを渡す ④描画件数を調整する。上から順に試して、改善したらそこで止めるのが正しいやり方です。

大前提:ScrollView + map をやめる

一番多い原因がこれです。ScrollView の中で map すると、画面外も含めて全アイテムが一度に生成される。100件あれば100件分のViewが即座に作られ、初期表示で固まります。

// ❌ 悪い例:全件を一度に描画する
<ScrollView>
  {items.map((item) => (
    <PostItem key={item.id} post={item} />
  ))}
</ScrollView>

// ✅ 良い例:FlatListは画面に見える分だけ描画する
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <PostItem post={item} />}
/>

? POINT

FlatListは「仮想化リスト」です。画面に表示される範囲+少しだけを描画し、スクロールに応じて使い回します。これが効くからこそ、ScrollView+mapより圧倒的に速くなります。
keyExtractorと行のmemo化

keyExtractor安定したキーを渡すこと。indexをキーにすると並び替えや追加でReactが余計な再描画をします。さらに行コンポーネントを React.memo で包むと、関係ない再レンダリングを止められます。

import { memo } from 'react';

// 行コンポーネントをmemo化
const PostItem = memo(function PostItem({ post }: { post: Post }) {
  return (
    <View style={styles.row}>
      <Text>{post.title}</Text>
    </View>
  );
});

注意点として、renderItem にインラインで重い関数やオブジェクトを毎回作るとmemoが効きません。onPressuseCallback で固定します。

const handlePress = useCallback((id: string) => {
  navigation.navigate('Detail', { id });
}, [navigation]);

// renderItemは外で定義 or useCallbackで固定
const renderItem = useCallback(
  ({ item }: { item: Post }) => (
    <PostItem post={item} onPress={handlePress} />
  ),
  [handlePress]
);
スカイ
スカイ
「memoしたのに効かない」ときは、だいたいrenderItemの中で毎回新しい関数を作ってるパターンだね。
getItemLayoutで描画を先読みさせる

行の高さが固定なら getItemLayout を渡します。各行の位置を測らずに計算で求められるため、スクロールが滑らかになり、特定位置へのジャンプも速くなります。

const ROW_HEIGHT = 72;

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
/>

⚠️ 注意

getItemLayout高さが全行で一定のときだけ使えます。可変高さの行に固定値を渡すとスクロール位置がズレるので使わないでください。
レンダリング件数のチューニング

最後に、それでも重いときだけ描画件数を調整します。やみくもに全部いじらず、初期表示が遅いなら initialNumToRender、スクロール時のカクつきなら windowSizemaxToRenderPerBatch を触ります。

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}
  initialNumToRender={8}      // 初期表示する件数
  maxToRenderPerBatch={8}     // 1バッチで追加描画する件数
  windowSize={5}              // 画面の何倍を保持するか
  removeClippedSubviews       // 画面外Viewを切り離す(Android向け)
/>

? この記事のまとめ
  • まずScrollView+mapをFlatListに置き換える(効果が一番大きい)
  • keyExtractorは安定キー、行はReact.memoで包む
  • 高さ固定ならgetItemLayoutで先読みさせる
  • 件数チューニングは最後の手段、症状に応じて該当プロパティだけ触る
Recommended
学んだことは、ブログに書くと一番定着する
エンジニアのアウトプット先として、表示速度の速いConoHa WINGでブログを始めました。月額わずか、最短10分で開設できます。
▶ ConoHa WINGでブログを始める
※ アフィリエイトリンクです
Next
次回:expo-imageで画像表示を速くする(大量画像のクラッシュ対策)
React Native実践シリーズ

コメント

タイトルとURLをコピーしました