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

はじめに
React を学んでいると、useMemo や useCallback というフックに出会います。どちらも「パフォーマンス最適化のためのフック」と説明されることが多いですが、実際にどんな問題を解決するのか、なぜ必要なのか、どんな場面で使い分けるのかが分からず、なんとなく使ってしまうことが少なくありません。
まず前提として、とても重要な事実があります。
React はレンダリングのたびに、コンポーネントの中身を上から全部もう一度実行します。
この仕様があるからこそ、useMemo と useCallback が必要になります。この記事では、初学者の方でも理解できるように、React のレンダリングの仕組みと結びつけながら、これらのフックの目的と使い方を丁寧に解説します。
参考にしたリンク
- https://react.dev/reference/react/useMemo
- https://react.dev/reference/react/useCallback
- https://react.dev/learn/render-and-commit
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は前回の関数を保存している- 依存配列が変わらない限り、同じものを使い回す
何をしているのか?(コードの視点)
コード | 何をしているか |
|---|---|
| 計算結果を保持している |
| 関数を保持している |
| 再計算の条件を指定している |
| 関数を props として渡している |
まとめ
- 入力中に重い計算を防ぐなら
useMemo - 子コンポーネントの再描画を防ぐなら
useCallback React.memoと組み合わせると効果が大きい
フック | 覚えるもの | 主な目的 |
|---|---|---|
useMemo | 計算結果 | 重い処理の再実行を防ぐ |
useCallback | 関数 | 子の再レンダリングを防ぐ |
useMemo と useCallback は、「React が毎回コンポーネントを実行する」という仕組みを前提に、その無駄を減らすためのフックです。レンダリングの仕組みとセットで理解することで、なぜこれらが必要なのかが、はっきり見えてきます。