第8回

useMemoでパフォーマンス最適化:重い計算をメモ化する

ReactのuseMemoを解説します。メモ化の仕組み・高コスト計算の最適化・参照の安定化、そしてuseMemoを使うべき場面・使わなくてよい場面の判断基準を実例付きで学べます。

·11分で読める

useMemoとは

useMemo計算結果をメモ化するHookです。依存配列の値が変わらなければ、前回の計算結果をそのまま返し、再計算をスキップします。

const memoizedValue = useMemo(() => {
  return expensiveCalculation(a, b);
}, [a, b]);  // a か b が変わったときだけ再計算

メモ化が必要なケース

重い計算

function ProductList({ products, filterText }: {
  products: Product[];
  filterText: string;
}) {
  // ❌ レンダリングのたびにフィルタリングが走る
  const filtered = products.filter((p) =>
    p.name.toLowerCase().includes(filterText.toLowerCase())
  );

  // ✅ filterText か products が変わったときだけ再計算
  const filtered = useMemo(() =>
    products.filter((p) =>
      p.name.toLowerCase().includes(filterText.toLowerCase())
    ),
    [products, filterText]
  );

  return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

ソートと集計

type SaleRecord = { date: string; amount: number; category: string };

function SalesDashboard({ records }: { records: SaleRecord[] }) {
  const [sortKey, setSortKey] = useState<'date' | 'amount'>('date');

  // sortKey か records が変わったときだけ再ソート
  const sorted = useMemo(() =>
    [...records].sort((a, b) => {
      if (sortKey === 'date') return a.date.localeCompare(b.date);
      return b.amount - a.amount;
    }),
    [records, sortKey]
  );

  // records が変わったときだけ再集計
  const total = useMemo(() =>
    records.reduce((sum, r) => sum + r.amount, 0),
    [records]
  );

  return (
    <div>
      <p>合計: ¥{total.toLocaleString()}</p>
      <button onClick={() => setSortKey('date')}>日付順</button>
      <button onClick={() => setSortKey('amount')}>金額順</button>
      <ul>{sorted.map((r, i) => <li key={i}>{r.date}: ¥{r.amount}</li>)}</ul>
    </div>
  );
}

オブジェクト・配列の参照を安定させる

useMemo は重い計算だけでなく、オブジェクトや配列の参照を安定させるのにも使えます。

function UserProfile({ userId }: { userId: number }) {
  const [theme, setTheme] = useState('light');

  // ❌ レンダリングのたびに新しいオブジェクトが作られる
  // → useEffect の依存配列に入れると無限ループになる
  const config = { userId, theme };

  // ✅ userId か theme が変わったときだけ新しいオブジェクトを作る
  const config = useMemo(() => ({ userId, theme }), [userId, theme]);

  useEffect(() => {
    // config が安定しているので、useIdやthemeが変わったときだけ実行される
    applyUserSettings(config);
  }, [config]);

  return <div>...</div>;
}

useMemoを使わなくてよいケース

軽い計算にuseMemoを使うとかえってパフォーマンスが悪化します。メモ化自体にも計算コストがかかるためです。

// ❌ これは不要(単純な計算はメモ化しない)
const doubled = useMemo(() => count * 2, [count]);

// ✅ シンプルに書けばよい
const doubled = count * 2;
// ❌ 要素数が少ない配列のフィルタリングもメモ化不要
const filtered = useMemo(() =>
  items.filter((item) => item.active),
  [items]
);

// ✅ items が多くなったら検討する
const filtered = items.filter((item) => item.active);

useMemoを使うべき目安

状況 判断
計算が明らかに重い(大量データの処理) 使う
参照の安定化が必要(useEffect依存配列) 使う
React.memoと組み合わせてprops変化を防ぐ 使う
単純な四則演算・短い配列操作 使わない
レンダリング頻度が低いコンポーネント 使わない

React.memoとの組み合わせ

React.memo はpropsが変わっていなければコンポーネントの再レンダリングをスキップします。useMemo と組み合わせると効果的です。

// React.memoでラップしたコンポーネント
const ExpensiveChart = React.memo(function Chart({ data }: { data: number[] }) {
  return <canvas>{/* 重い描画処理 */}</canvas>;
});

function Dashboard({ rawData }: { rawData: RawRecord[] }) {
  const [filter, setFilter] = useState('all');

  // useMemo でデータを安定させないと React.memo が効かない
  const chartData = useMemo(() =>
    rawData
      .filter((r) => filter === 'all' || r.category === filter)
      .map((r) => r.value),
    [rawData, filter]
  );

  return (
    <div>
      <select value={filter} onChange={(e) => setFilter(e.target.value)}>
        <option value="all">すべて</option>
        <option value="A">カテゴリA</option>
      </select>
      {/* chartData が変わったときだけ再レンダリングされる */}
      <ExpensiveChart data={chartData} />
    </div>
  );
}
なつみ
なつみ UIの実装だと、大量リストのフィルタリングで使うことが多い。スクロールが多いページで検索入力のたびに数千件を再フィルタしてると体感で重くなる。でも最初から `useMemo` を全部に入れるのはやりすぎ。
たける
たける 最初はつけずに、重くなったらつけるってことですか?
なつみ
なつみ そう。DevToolsのProfilerで実際に計測してから判断する。体感で重くないなら触らない。最適化は測定が先。
りこ
りこ useMemoを入れ過ぎるとコードが読みにくくなる上に、メモ化コスト自体がかかる。「軽い計算を大量にメモ化する」のは逆効果になることもある。

まとめ

  • useMemo は依存配列の値が変わらなければ計算結果をキャッシュする
  • 重い計算(大量データの処理・ソート)や参照の安定化に使う
  • 軽い計算には使わない(メモ化コスト > 計算コストになる)
  • React.memo と組み合わせると子コンポーネントの不要な再レンダリングを防げる

次の第9回では、関数をメモ化する useCallback と、React.memo との組み合わせを学びます。