フィードに20個のアイテムがある状態は、完璧に感じられます。スクロールは滑らかで、クライアントも満足しているでしょう。しかし、いざ本番環境にデプロイし、データが届くと、突然2,000行ものデータと向き合うことになります。UIはカクつき始め、メモリ使用量は徐々に増え続け、最終的にOSによってアプリが強制終了されます。焦った開発者の中には、すべてを ScrollView で包んでその場を凌ごうとする人もいますが、その決断は、解決した1つのバグに対して3つの新しいバグを生み出すのが常です。
リストは、ユーザーが React Native アプリをどのように感じるかを決定づけるパフォーマンスのボトルネックです。正しく実装すれば、アプリはネイティブのように感じられます。実装を誤れば、どんなに美しい画面でも動作が重くなってしまいます。根本的な原因は、選択したコンポーネントと、JavaScript スレッドおよび UI スレッドに要求している作業内容との間のミスマッチであることがほとんどです。React Native は2つのトラックで動作します。ロジックは JS スレッド上で動作し、描画(painting)はネイティブの UI スレッド上で行われます。巨大なリストを誤った方法でレンダリングすると、両方のスレッドがレイアウト計算、再レンダリング、メモリ割り当てによって処理しきれなくなります。その結果、フレームドロップ、画面の白飛び、そして最終的にはクラッシュが発生します。
Pick the Right Tool
リストコンポーネントの選択は、反射的な判断ではなく、意図的なアーキテクチャ上の決定であるべきです。
ScrollView は最もシンプルな選択肢です。渡されたすべての要素を即座にメモリにマウントし、その塊を丸ごとネイティブのスクロールエンジンに渡します。設定画面、ログインフォーム、あるいは10セクション程度の静的な製品詳細ページのような、短くて固定されたコンテンツには最適です。予測可能でスタイリングも簡単です。欠点は仮想化(virtualization)がないことです。2,000個のアイテムを渡せば、忠実に2,000個のネイティブビューを作成します。大規模または動的なデータセットには ScrollView を使用しないでください。それは「図書棚」ではなく「額縁に入ったポスター」だと考えてください。
FlatList は、長くて均一なフィードのための主力製品です。コンテンツを仮想化するため、現在表示されている、あるいはビューポートに近い行のみをマウントします。ユーザーがスクロールすると、FlatList は画面から消えたセルをアンマウントし、それらを新しいデータのために再利用します。これにより、配列がどれほど大きくなってもメモリ使用量は一定に保たれます。ソーシャルタイムライン、通知センター、あるいは似たようなカードが連続してスクロールするコレクションを作成している場合は、FlatList がデフォルトの正しい選択肢です。
SectionList は、整理整頓された FlatList です。アルファベット順に並んだアドレス帳、日付ごとに分かれたワークアウトログ、月ごとに整理された請求書リストなど、データがグループ化された状態で届く場合に使用します。スティッキーなセクションヘッダーをレンダリングし、グループ化のロジックを自動的に処理してくれます。内部的には FlatList と同じ仮想化エンジンを使用しているため、メモリのメリットを享受しつつ、タイトル付きのパーティションという構造的な利点を得られます。
FlashList は、デバイスから最後の一滴までフレームを絞り出したい時に登場します。RecyclerListView エコシステムに基づいて構築されており、FlatList よりも積極的にビューを再利用し、ミドルレンジのハードウェアでも 60 FPS を維持することを目指しています。大量のメッセージが流れるチャットインターフェース、スクロール速度が速い製品カタログ、あるいは滑らかさが競争優位性となるような画面を構築している場合、FlashList は追加の依存関係を導入する価値があります。すべての画面に必要というわけではありませんが、コア体験を定義するフィードにおいては、パフォーマンスの差が顕著に現れます。
Common Performance Killers
リストの動作が重くなり始めたとき、疑われるべき「3人の容疑者」がいます。
一度に大量の React ツリーをマウントすることは、最も劇的な失敗です。すべての行が複雑なコンポーネントツリーである場合、初期レンダリングが JS スレッドを長時間ブロックし、真っ白な画面や、表示が遅れるといった現象を引き起こす可能性があります。ユーザーはアプリを開いて待ち続けることになります。初期ロード後であっても、重い行はスクロールの初期化を遅らせます。なぜなら、最初の数フレームがセットアップ作業に費やされてしまうからです。
1フレームあたりの作業量が多すぎると、スクロール中のカクつきとして現れます。アニメーションを滑らかに保つには、1フレームあたり約 16 ミリ秒の予算があります。行コンポーネント内で高コストな計算を実行したり、その場で日付をパースしたり、レンダリング内で深いオブジェクト比較を行ったりすると、この予算を使い果たしてしまいます。UI スレッドでフレームドロップが発生し、ユーザーはガクつきを感じることになります。
過剰なメモリ使用は「静かな殺し屋」です。すべてのネイティブビューには RAM が必要です。最適化されていない大きな画像を追加したり、すべてのカードにドロップシャドウを付けたり、ネストされた Touchable を使用したりすると、メモリ使用量は倍増します。iOS では、システムが警告なしにアプリを強制終了することがあります。Android では、ユーザーがラグの蓄積を目の当たりにし、アプリが使い物にならなくなっていくのを待つことになります。
Optimization Checklist
「単に動作するだけのリスト」と「爆速で動くリスト」を分けるのは、小さな戦略的習慣です。
Use stable keys. データセットから実際の識別子を常に key プロップに渡してください。配列のインデックスは決して使用しないでください。リストの並べ替え、フィルタリング、または項目の追加が発生した場合、インデックスベースのキーを使用していると、Reactが誤ったデータを誤った再利用コンポーネントと紐付けてしまう原因になります。このミスにより、不要なアンマウント、ステートの不一致、そして連鎖的な再レンダリングが発生します。適切なIDがあれば、Reactはどの行がどこに移動したかを正確に把握できます。
Memoize rows. 行のコンポーネントを React.memo でラップし、プロップスが実際に変更されたときのみ再レンダリングされるようにします。このガードがないと、親のステートが更新されるたびに、たとえデータが同一であっても、表示されているすべての行に対してレンダリングが走ってしまいます。更新の激しい長いリストでは、これらの無駄なサイクルがすぐに蓄積していきます。
Keep renderItem stable. 親のレンダリングのたびに、renderItem プロップの中で新しい関数を直接定義することを避けてください。renderItem={({ item }) => <Row data={item} />} のようなインラインのアロー関数は、親が更新されるたびに新しい参照を作成します。FlatListはプロップスが変更されたと判断し、不要に行を再利用してしまいます。レンダリング関数をコンポーネントの外で定義するか、useCallback でメモ化して参照を安定させてください。
Use getItemLayout whenever possible. 行の高さが固定されている、または予測可能な場合は、その値を正確に FlatList に伝えてください。このプロップを使用することで、リストはコストの高いネイティブの計測処理をスキップできます。マウント後に各セルを計測する代わりに、リストは数学的に位置を計算します。この差は、数百から数千の項目を持つリストで特に顕著になります。そのようなリストでは、onLayout の頻繁な呼び出しが JS スレッドを圧迫する原因となります。
Optimize images aggressively. サイズ指定のない画像は、リストにとって毒となります。画像がデコードされる前にネイティブレイヤーがスペースを確保できるよう、常に明示的な幅と高さを設定してください。リモート画像については、メモリキャッシュ、ディスク永続化、フォーマット最適化を処理できる Expo Image や同等のキャッシュライブラリを使用してください。デフォルトの React Native Image コンポーネントはプロトタイプには適していますが、本番環境のフィードでは、メモリやローディング状態をより細かく制御する必要があります。
Avoid nesting scroll containers. 垂直方向の FlatList を垂直方向の ScrollView の中に入れないでください。親の ScrollView がすべてのスクロールイベントをキャプチャしてしまうため、子である FlatList がビューポートを計測できなくなります。FlatList がどの行を表示すべきか判断できなくなるため、仮想化(Virtualization)が機能しなくなります。その結果、結局すべての行がマウントされてしまい、仮想化の目的そのものが台無しになります。リストの上にヘッダーが必要な場合は、FlatList 自体の ListHeaderComponent プロップを使用してください。複雑なスティッキー(固定)動作が必要な場合は、適切なヘッダー設定を備えた SectionList または FlashList を使用してください。
The Golden Rule
コンテンツが少なく限定的な場合は、ScrollView に任せてください。ユーザー生成データやリモートのページネーションによってコンテンツが増える場合は、仮想化リストを使用してください。リストがアプリの主役であり、ユーザーが数分間にわたってスクロールし続けるような場合は、FlashList を検討してください。
忘れがちな、最後のアドバイスがあります。シンプルな行ほど、スクロールは速い。行のコンポーネントを軽量にすればするほど、リストはスムーズになります。個々の行から、ネストされたナビゲーション、重い計算、不必要なアニメーションを削ぎ落としてください。マークアップはフラットに、ロジックは薄く、画像にはサイズを指定します。リストの成否は、レンダリングされるものの累積的な重みによって決まります。各行のコストを最小限に抑えれば、リストは最高級の(贅沢なほどスムーズな)体験を提供してくれるでしょう。
