RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-02中級

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

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

FlashList5FlatList9パフォーマンス30React Native209Rork515

リストが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 で楽になる」設計です。

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 では不要
      // 画像付きカードでも 1000 件スクロールが滑らかになる
    />
  );
}

ポイントは「FlatList の最適化プロパティをほぼ全部消せる」ことです。windowSize initialNumToRender removeClippedSubviews getItemLayout などのチューニングは 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 でも引き続き重要です。これを書いておくと、画像カードと文字カードがそれぞれ別のリサイクルプールで管理されるため、内部の DOM 構造の差異が原因で再描画が走ることがなくなります。私は最初これを省略して、画像と文字が混ざったカードで一瞬チラつく現象に悩まされたので、種類が明確に分かれるリストには必ず付けることをおすすめします。

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

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

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

2つ目は keyExtractor をインライン関数で都度作ってしまうことです。 一見問題なさそうに見えますが、レンダリングごとに新しい関数参照が生成されるため、v2 のリサイクル判定が混乱します。コンポーネント外で const keyExtractor = (item: Post) => item.id のように定義するか、useCallback でラップしてください。

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 にリセットすると安全です
  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 が画面右上に表示されます。

// パフォーマンス計測用のラッパー(開発ビルドのみ)
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 時は JS スレッドが 35〜40fps だったのが、FlashList v2 移行後は 58〜60fps で安定しました。スクロール中に新しい画像が読み込まれる場面でも、UI スレッドのフレームドロップがほぼなくなりました。

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

最後にお伝えしたいのは、FlashList v2 への移行はアプリ全体を一度に置き換える必要はないということです。最もスクロールが重いリスト1つだけを差し替えて、効果を計測します。それで満足できれば次のリスト、というように段階的に進めるのが現実的です。

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

シェア

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

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

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

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

関連記事

開発ツール2026-06-19
Rork で作ったリストのスクロールが重い — 画像キャッシュとプリフェッチの設計
Rork が生成した FlatList は画像が増えるとスクロールがカクつきます。expo-image のキャッシュ、recyclingKey、プリフェッチ、FlashList への移行を実機の数値とともに整理し、滑らかさを取り戻すまでの設計を残しました。
開発ツール2026-05-22
FlatList の onEndReached が連続発火して API を叩きすぎる問題の原因と対処
FlatList の onEndReached が画面遷移時や初期描画時に何度も呼ばれて、ページネーション API を多重に叩いてしまう問題。Rork で生成したリスト画面でも頻発するこの挙動の原因と、実運用で使える対処パターンを整理しました。
開発ツール2026-05-01
VirtualizedList を ScrollView の中に置くと出る警告 — Rork で正しく直すパターン集
Rork のアプリで FlatList を ScrollView に入れたら出る『VirtualizedLists should never be nested』警告。原因と、ListHeaderComponent や FlashList を使った正しい解決パターンを実例で解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →