KAKUTA TECH BLOG

Blog Article

useMemo / useCallback : パフォーマンス最適化の目的と使い方を理解

はじめに

React を学んでいると、useMemouseCallback というフックに出会います。どちらも「パフォーマンス最適化のためのフック」と説明されることが多いですが、実際にどんな問題を解決するのか、なぜ必要なのか、どんな場面で使い分けるのかが分からず、なんとなく使ってしまうことが少なくありません。

まず前提として、とても重要な事実があります。

React はレンダリングのたびに、コンポーネントの中身を上から全部もう一度実行します。

この仕様があるからこそ、useMemouseCallback が必要になります。この記事では、初学者の方でも理解できるように、React のレンダリングの仕組みと結びつけながら、これらのフックの目的と使い方を丁寧に解説します。

参考にしたリンク

useMemo / useCallback とはなにか?

一言で言うと、

useMemo は「計算結果を覚えておくフック」です。

useCallback は「関数そのものを覚えておくフック」です。

どちらも、「毎回同じものを作り直さないようにする」ための仕組みです。

なぜ必要なのか?・それぞれのフックがないと、どうなるのか?

React は state が更新されるたびに、コンポーネント関数を最初から最後まで実行します。これは React の正しい動きですが、この挙動によって次のような問題が起きます。

たとえば、コンポーネントの中で重い計算をしていた場合、state が少し変わるだけで、その計算が毎回実行されてしまいます。本来は関係のない更新でも、無駄な計算が発生します。

また、関数を子コンポーネントに渡している場合、レンダリングのたびに「新しい関数」が作られます。すると、子コンポーネントは「受け取った props が変わった」と判断し、再レンダリングされてしまいます。

useMemo がない場合は、「毎回同じ計算を繰り返す」問題が起きます。

useCallback がない場合は、「毎回新しい関数が作られ、子が無駄に再レンダリングされる」問題が起きます。

どんな場面で使うか、使い分けは以下のとおりです。

  • 重い計算の結果を再利用したいときは useMemo
  • 子コンポーネントに渡す関数を固定したいときは useCallback
  • React.memo と組み合わせて再レンダリングを防ぎたいとき
  • 配列やオブジェクトを props として渡すとき

コード例で比較

useMemo を使わない場合

// App.jsx
import { useState } from "react";

function heavyCalculation(num) {
  console.log("計算中...");
  for (let i = 0; i < 1000000000; i++) {}
  return num * 2;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const result = heavyCalculation(count);

  return (
    <div>
      <p>結果 : {result}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

このコードでは、入力欄に文字を入力するだけで、heavyCalculation が毎回実行されます。count は変わっていないのに、無駄な計算が発生します。

useMemo を使う場合

// App.jsx
import { useState, useMemo } from "react";

function heavyCalculation(num) {
  console.log("計算中...");
  for (let i = 0; i < 1000000000; i++) {}
  return num * 2;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const result = useMemo(() => {
    return heavyCalculation(count);
  }, [count]);

  return (
    <div>
      <p>結果 : {result}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

count が変わったときだけ、計算が実行されます。

ここで行っていることは、「計算結果の保持」です。

useCallback を使わない場合

// Parent.jsx
import Child from "./Child";

export default function Parent() {
  const handleClick = () => {
    console.log("クリック");
  };

  return <Child onClick={handleClick} />;
}

レンダリングのたびに、新しい handleClick が作られます。

useCallback を使う場合

// Parent.jsx
import { useCallback } from "react";
import Child from "./Child";

export default function Parent() {
  const handleClick = useCallback(() => {
    console.log("クリック");
  }, []);

  return <Child onClick={handleClick} />;
}

関数の参照が固定され、子コンポーネントの無駄な再レンダリングを防げます。

何が起きているのか?(裏側の動き・データの流れ)

  • コンポーネントが再実行される
  • 通常なら計算や関数が毎回作られる
  • useMemo は前回の計算結果を保存している
  • useCallback は前回の関数を保存している
  • 依存配列が変わらない限り、同じものを使い回す

何をしているのか?(コードの視点)

コード

何をしているか

useMemo(() => fn, [deps])

計算結果を保持している

useCallback(() => fn, [deps])

関数を保持している

[count]

再計算の条件を指定している

Child onClick={handleClick}

関数を props として渡している

まとめ

  • 入力中に重い計算を防ぐなら useMemo
  • 子コンポーネントの再描画を防ぐなら useCallback
  • React.memo と組み合わせると効果が大きい

フック

覚えるもの

主な目的

useMemo

計算結果

重い処理の再実行を防ぐ

useCallback

関数

子の再レンダリングを防ぐ

useMemouseCallback は、「React が毎回コンポーネントを実行する」という仕組みを前提に、その無駄を減らすためのフックです。レンダリングの仕組みとセットで理解することで、なぜこれらが必要なのかが、はっきり見えてきます。