🦁

【React Native】FlatListのスクロールが重すぎた話 useMemoの有用性

に公開

初めに

初めて個人開発をしたアプリMameLabを作るとき、パフォーマンスの問題に直面しました。
今回はその時学んだことについて書きます。

▼ アプリのダウンロードはこちら
https://play.google.com/store/apps/details?id=com.coffee.mamelog&hl=ja

直面した課題

予定のレシピを記述する画面(RecipeSetup)で、画面のスクロールがカクつくという現象に悩まされました。余りにもレスポンスが悪く、アプリのコアな部分であったため、アプリの印象に悪影響でした。

原因の調査

原因は、「レンダリングのたびに計算のロジックが走っていたこと」 でした。このアプリでは、ユーザーが豆の量や抽出量の設定を変化させたときに、一旦オススメのレシピを表示するようにしていました。しかし、この計算処理をコンポーネント内に直接書いていたため、スクロールなど些細な再レンダリングのたびに毎度計算が実行されていました。

Before

export default function HomeScreen() {
  const [dose, setDose] = useState(15); // 豆の量
  const [targetWater, setTargetWater] = useState(230); // 注湯量

  // 😱 問題点:
  // レンダリングのたびに計算が走る。
  // さらに毎回新しい配列が生成されるため、FlatListも全再描画される。
  const recipeSteps = generatePourPlan(dose, targetWater, '4:6_method');

  return (
    <View>
      {/* 以下のFlatListがスクロール時にカクつく */}
      <FlatList
        data={recipeSteps}
        renderItem={({ item }) => <StepRow item={item} />}
      />
    </View>
  );
}

After

useMemoを導入することで、計算に必要なパラメータが変化したときだけ再計算するように変更しました。

export default function HomeScreen() {
  const [dose, setDose] = useState(15);
  const [targetWater, setTargetWater] = useState(230);

  // 😇 解決策:
  // dose または targetWater が変更された時だけ再計算する。
  // それ以外の再レンダリング(スクロール等)では、前回の計算結果(キャッシュ)を使い回す。
  const recipeSteps = useMemo(() => {
    return generatePourPlan(dose, targetWater, '4:6_method');
  }, [dose, targetWater]);

  return (
    <View>
      {/* データ参照が安定したため、FlatListのパフォーマンスも向上 */}
      <FlatList
        data={recipeSteps}
        renderItem={({ item }) => <StepRow item={item} />}
      />
    </View>
  );
}

これにより無駄な再計算がなくなり、フレームレートが改善されました。

そもそもuseMemoとは

Reactには、再レンダリングされると、関数や変数は全て作り直されるという性質があります。

通常、変数の再定義くらいなら一瞬で終わります。しかし、今回のような「重い計算処理」や「配列のソート」が含まれる場合、ユーザーが少し画面に触れるたびに裏ですごい計算が走ることになり、これがパフォーマンスの低下につながっています。

そこで登場するのがuseMemoです。これは計算結果をメモしておくフック(特殊な関数)です。

const cachedValue = useMemo(() => {
  return heavyCalculation(a, b);
}, [a, b]); // ← 依存配列
  1. 初回レンダリング時:計算を実行し、結果を保存します。
  2. 2回目以降:依存配列([a, b])の中身をチェックします。
    • 中身が変わっていなければ:計算をせず、保存してあった結果を返します
    • 中身が変わっていたら:再計算を実行し保存内容を更新します

今回のケースでは 「豆の量(dose)や注湯量(targetWater)が変わらない限り、スクロール等で画面が再描画されてもレシピ計算はしない」 という制御により、パフォーマンス改善に繋がりました。

まとめ

  • 動的に生成するデータはuseMemoを活用する。
  • 特にFlatListに渡すデータは、不必要に新しい配列を作らない。

Discussion