◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-05-02中級

FlatList でカクつき始めたら読む — Rork アプリを FlashList v2 で快適にする実装手順

Rork で生成したアプリの長いリストが重くなったとき、FlashList v2 へ移行することでスクロールが大幅に滑らかになります。estimatedItemSize が不要になった v2 の思想を踏まえた、現実的な移行手順を解説します。

FlashList5FlatList10パフォーマンス33React Native239Rork584

リストが300件を超えたあたりから、急にカクつき始める

Rork で日記アプリの初期版を作って、サンプルデータで動作確認している時はとても快適でした。ところがユーザーが200件、300件と投稿を貯めていくと、スクロールが目に見えて重くなり、画像付きのカードを並べたタイムラインでは下にスワイプするたびに白いプレースホルダーが一瞬見えるようになりました。

最初は画像のキャッシュが原因かと疑いました。実際には FlatList のレンダリング負荷が上限に達していたのです。同じデータで FlashList v2 に置き換えたところ、スクロールは滑らかになり、メモリ使用量も明確に下がりました。Rork で生成したアプリを FlashList v2 へ移行するときの考え方と、私が実際に詰まったポイントを書き残します。

FlatList が遅くなる仕組みは、見えていないところにあります

FlatList は React Native 標準のリストですが、内部では VirtualizedList を継承して、画面外のアイテムをアンマウントする仕組みで動きます。問題は、各アイテムをマウントするたびに React のリコンサイル処理と JS スレッドでのレイアウト計算が発生することです。

カードに画像が入っていて、Reanimated のアニメーションが乗っていて、TouchableOpacity が複数あるような構造だと、1アイテムのマウントだけで数ミリ秒を消費します。これが画面に20件並ぶだけで、スクロールが指に追従しないレベルの遅延に化けます。

FlashList は Shopify が開発したリストで、この問題を「セルのリサイクル」というアプローチで解決しています。アイテムをアンマウントするのではなく、既存のセルを再利用してデータだけ差し替える、UIKit や RecyclerView と同じ思想です。v2 ではレイアウトの計測が実行時に自動で行われるようになり、estimatedItemSize を渡さなくても動きます。

v2 で変わった一番大きいこと: estimatedItemSize は書かなくていい

FlashList v1 を触った経験がある方なら、estimatedItemSize が必須プロパティだったことを覚えているはずです。これは初回レンダリングで「だいたいこのくらいの高さ」というヒントを渡すものでしたが、値を間違えると画面に空白ができたり、スクロール位置が飛んだりする原因になっていました。

v2 ではこのプロパティが不要になり、描画されたセルを内部で測りながら高さを見積もる方式に変わりました(コンパイラが計算するわけではありません)。私の感覚では「v1 で苦労した人ほど v2 で楽になる」設計です。

移行の前に確かめておくこと

v2 は New Architecture(Fabric)を前提に作り直されたバージョンです。旧アーキテクチャのままのプロジェクトでは v2 を使えず、v1 に留まる必要があります。Expo SDK と React Native のバージョンは、手元の package.json と公式の互換表で一度突き合わせてください。この確認を飛ばして npx expo install を走らせ、ビルドエラーの前で手が止まった夜がありました。

差分を整理すると、次のようになります。

項目FlatListFlashList v1FlashList v2
画面外のセルアンマウントして破棄リサイクルして再利用リサイクルして再利用
高さの見積もりgetItemLayout で任意指定estimatedItemSize が必須指定不要(実行時に計測)
前提アーキテクチャどちらでも可どちらでも可New Architecture が前提
セルの状態リセット不要(都度マウント)自前で対処useRecyclingState も利用可

表の内容は私が確認した時点のものです。細部は FlashList の公式ドキュメントで最新の記述をご覧ください。

Rork で生成したアプリへの移行手順

Rork はテンプレートで FlatList を使うことが多いので、まずはインストールから始めます。

# プロジェクトのルートで実行
npx expo install @shopify/flash-list
 
# Reanimated を併用している場合は最新版に揃える
npx expo install react-native-reanimated

次に、FlatList を使っているコンポーネントを差し替えます。最小構成のビフォーアフターです。

// Before — FlatList 版
import { FlatList } from "react-native";
 
export function PostList({ posts }: { posts: Post[] }) {
  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
      // 画面外プリロード距離・初期表示数なども調整していたが効果は限定的
      windowSize={10}
      initialNumToRender={8}
      removeClippedSubviews
    />
  );
}
// After — FlashList v2 版
import { FlashList } from "@shopify/flash-list";
 
export function PostList({ posts }: { posts: Post[] }) {
  return (
    <FlashList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
      // estimatedItemSize は v2 では不要
      // 画像付きカードでも長いリストが滑らかに動く
    />
  );
}

ポイントは「FlatList の最適化プロパティをほぼ全部消せる」ことです。windowSize initialNumToRender removeClippedSubviews getItemLayout といった FlatList 向けのチューニングは、FlashList に持ち込む必要がありません。私は移行時に古いプロパティを残してしまい、警告が出ても見落としていた経験があるので、置換後に必ず一度コンソールログを確認してください。

高さがバラバラなカードでも壊れない書き方

FlashList v2 で最も体感差が出るのは、アイテムごとに高さが変わるリストです。SNS のタイムラインのように、テキストだけのカードと画像付きカードが混在するリストを考えます。

type FeedItem =
  | { type: "text"; id: string; body: string }
  | { type: "image"; id: string; uri: string; caption: string };
 
export function Timeline({ items }: { items: FeedItem[] }) {
  return (
    <FlashList<FeedItem>
      data={items}
      keyExtractor={(item) => item.id}
      // getItemType を渡すとセルのリサイクル効率が大きく上がります
      getItemType={(item) => item.type}
      renderItem={({ item }) => {
        if (item.type === "image") return <ImageCard item={item} />;
        return <TextCard item={item} />;
      }}
    />
  );
}

getItemType は v1 から存在するプロパティですが、v2 でも引き続き重要です。これを書いておくと、画像カードと文字カードがそれぞれ別のリサイクルプールで管理されるため、構造の違うセル同士で中身を無理に入れ替える無駄が減ります。私は最初これを省略して、画像と文字が混ざったリストで一瞬チラつく現象に悩まされました。種類が明確に分かれるリストには、付けておくと安心です。

詰まりやすい3つのポイント

私が実際に踏んだ落とし穴を共有します。同じ罠を踏まずに済むはずです。

1つ目は ScrollView の中にネストしてしまうことです。 これは VirtualizedList 全般に言える話ですが、FlashList も例外ではありません。<ScrollView> の中に FlashList を置くと、仮想化が無効になって全件マウントされ、結局 FlatList より重くなることがあります。スクロールが必要な親レイアウトが欲しい場合は、ListHeaderComponent と ListFooterComponent で代替することを徹底してください。詳しい背景は VirtualizedList のネスト警告を解消する でも触れています。

2つ目は、行コンポーネントに key を付けてしまうことです。 FlatList の癖で renderItem の返す要素に key={item.id} を書くと、リサイクルのたびにセルが作り直されてしまい、FlashList の利点が消えます。識別は keyExtractor に任せ、行の側では key を書かないようにしています。ついでに keyExtractor もコンポーネントの外で const keyExtractor = (item: Post) => item.id と定義しておくと、毎回の関数生成を避けられます。

3つ目は data 配列の不変性です。 FlashList は data 参照の変化を見て差分を計算します。並び替えや絞り込みで新しい配列を作るときは setItems([...items, newItem]) のように毎回新しい配列を返すことが前提です。items.push(newItem) のような破壊的更新だと再描画されません。これは FlatList でも同じですが、FlashList は内部最適化が強い分、ミスが目に見えにくいので注意してください。Zustand や Redux に配列を保存している場合は、セレクタが描画ごとに同じ参照を返すかも合わせて確認してください。参照が毎回変わるとリサイクルプールが乱れて、せっかくの v2 の恩恵が消えてしまいます。

Reanimated とジェスチャー付きカードを扱うときの注意

Rork で作るアプリでは、スワイプ操作や拡大アニメーションを Reanimated で組み込んだカードが頻繁に登場します。FlatList ではジェスチャーの状態を維持するために再描画と戦う場面が多かったのですが、FlashList v2 ではほとんどそのまま動きます。ただし2点だけ気をつけたい点があります。

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { FlashList } from "@shopify/flash-list";
 
function SwipeableCard({ post }: { post: Post }) {
  const offset = useSharedValue(0);
  const cardStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: withSpring(offset.value) }],
  }));
  // セルがリサイクルされて別アイテムに変わったときは
  // post.id を依存にした useEffect 等で offset.value を 0 にリセットすると安全です
  // (通常の useState の代わりに useRecyclingState を使う方法もあります)
  return <Animated.View style={cardStyle}>{/* カード本体 */}</Animated.View>;
}

1点目は、共有値を「セル単位」ではなく「アイテム単位」で初期化することです。FlashList が別の投稿用にセルを再利用するとき、コンポーネントのインスタンスは残ったまま props だけが切り替わります。ジェスチャー状態を ref に保持してしまうと、リサイクルされたカードが前のカードのスワイプ量を引き継いだ状態で表示されます。post.id を依存にした副作用で共有値を初期化するのが最も安全です。

2点目は、重いカードを React.memo で包むことです。FlashList は行コンポーネントを自動でメモ化しないため、親の再描画が起きると見えている全カードが再描画される可能性があります。memo と安定した keyExtractor の組み合わせで、無駄な再描画を確実に減らせます。

体感ではなく数値で確認する

移行の効果は、感覚だけでなく React Native の Performance Monitor で確認できます。開発ビルドでデバイスを振って Show Perf Monitor を有効にすると、JS スレッドと UI スレッドの FPS が画面右上に表示されます。

// 簡易な計測用ラッパー(開発ビルドのみ。厳密な FPS 計測ではなく目安です)
import { InteractionManager } from "react-native";
 
export function trackScrollPerformance(label: string) {
  const start = Date.now();
  return InteractionManager.runAfterInteractions(() => {
    const elapsed = Date.now() - start;
    if (__DEV__) {
      console.log(`[perf] ${label}: ${elapsed}ms`);
    }
    return elapsed;
  });
}

私の手元の端末では、500 件の画像カードリストで、FlatList 時に 35〜40fps だったものが、FlashList v2 への移行後は 58〜60fps 付近で安定しました。数値は端末とカードの重さで大きく変わるため、あくまで一例です。ご自身のアプリでは、移行の前後に同じ操作で測り比べてみてください。

移行は1コンポーネントずつ、スクロールが重い箇所から

最後にお伝えしたいのは、FlashList v2 への移行はアプリ全体を一度に置き換える必要はないということです。重いリストを一つ選び、置き換えて、数字で見届けます。 この順番だけは崩さないようにしています。全部を一度に直そうとして、どこが効いたのか分からなくなったのだと、あとで気づいたことがありました。最もスクロールが重いリスト1つだけを差し替えて、効果を計測します。それで満足できれば次のリスト、というように段階的に進めるのが現実的です。

今日まず試すとしたら、あなたのアプリで一番長くなりがちなリスト画面を1つ選んで、上の最小構成のサンプルだけを適用してみてください。30分以内で差し替えが終わり、次にアプリを開いた瞬間に違いが分かるはずです。アニメーションのあるカードを並べているなら、Reanimated 3 と Gesture Handler の組み合わせ方 も併せて見直すと、さらにスムーズな体験に近づきます。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-07-24
同じ通信が同時に何本も飛ぶのを1本に束ねる — 参照カウント付きシングルフライト層の設計
起動直後に同じ API へ何本ものリクエストが同時に飛ぶ多重フェッチを、進行中の Promise を共有して1本に束ねる設計です。束ねる範囲を決めるキーの組み立て方、失敗した Promise を配り続けてしまう罠、呼び出し側の中断が全体を巻き込む罠、そして束ね率を数えて回帰を止めるテストまで、参照カウント付きシングルフライト層の実装を実測の通信ログとともに残します。
⬡ 開発ツール2026-04-25
Rork アプリでスケルトンスクリーンを実装する — 読み込み中の体感速度を改善する実践パターン
Rork で作ったアプリの「読み込み中」が、ただのぐるぐるスピナーで終わっていませんか。スケルトンスクリーンと Shimmer エフェクトを取り入れるだけで、体感速度と離脱率は大きく変わります。実装の型と注意点を整理しました。
⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。