はじめに
Reactアプリケーションを開発していると、「なんだか動作が重い」「入力時にカクつく」といった問題に直面することがあります。その原因の多くは、不要な再レンダリングにあります。
この記事では、Reactのレンダリング最適化の基本的な手法であるReact.memo、useCallback、useMemoについて、実践的なコード例とともに解説します。
この記事の対象読者
- ReactのStateやPropsの基本は理解している
- コンポーネントが再レンダリングされる仕組みを知りたい
- パフォーマンス改善の具体的な手法を学びたい
学べる内容
- レンダリングを可視化する方法
- 再レンダリングが発生する条件
- React.memo、useCallback、useMemoの使い方と使い分け
前提条件
Reactのバージョンについて
この記事のコード例はReact 19で動作確認しています。
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
React 19では、React Compilerによる自動最適化が段階的に導入されていますが、従来の最適化手法(memo、useCallback、useMemo)も引き続き使用できます。この記事では、これらの基本的な最適化手法について学んでいきます。
必要な基礎知識
- React Hooksの基本(useState、useEffect)
- コンポーネントとPropsの概念
- JavaScriptの参照型と値型の違い
レンダリングの基本を理解する
最適化を行う前に、まずはレンダリングがいつ発生しているのかを可視化してみましょう。
レンダリングを可視化する準備
レンダリングを確認する方法は主に2つあります。
1. React DevToolsを使う方法
Chrome拡張機能のReact DevToolsをインストールし、設定で「Highlight updates when components render」を有効にします。
2. console.logを使う方法
シンプルですが、コンポーネント関数の先頭にログを仕込むことで確認できます。
function MyComponent() {
console.log('MyComponent rendered');
return <div>Hello</div>;
}
この記事では、より明確に理解するためconsole.logを使った方法で進めます。
レンダリングコストの高いコンポーネントを作成
実際のアプリケーションでは、計算処理が重いコンポーネントや大量のデータを扱うコンポーネントが存在します。ここでは模擬的に重い処理を行うコンポーネントを作成してみましょう。
// 重い計算処理を模擬する関数
function heavyCalculation(num) {
console.log('重い計算を実行中...');
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += num;
}
return result;
}
function HeavyComponent({ count }) {
console.log('HeavyComponent rendered');
const result = heavyCalculation(count);
return (
<div>
<h3>重いコンポーネント</h3>
<p>計算結果: {result}</p>
</div>
);
}
このコンポーネントは、親コンポーネントが再レンダリングされるたびに重い計算を実行してしまいます。これが最適化が必要な理由です。
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
return (
<div>
<h1>レンダリング最適化のデモ</h1>
{/* このinputに入力するたびにHeavyComponentも再レンダリングされる */}
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="テキストを入力"
/>
<button onClick={() => setCount(count + 1)}>
カウント: {count}
</button>
<HeavyComponent count={count} />
</div>
);
}
export default App;
この状態でinputに文字を入力すると、入力のたびにHeavyComponentが再レンダリングされ、重い計算が実行されてしまいます。コンソールを確認すると、「HeavyComponent rendered」と「重い計算を実行中...」が連続して表示されるはずです。
再レンダリングが発生する条件
Reactのコンポーネントが再レンダリングされる主な条件は以下の3つです。
1. 自身のStateが更新されたとき
function Counter() {
const [count, setCount] = useState(0);
// setCountが呼ばれるとCounterが再レンダリングされる
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2. 親コンポーネントから受け取るPropsが変更されたとき
function ChildComponent({ name }) {
console.log('ChildComponent rendered');
return <div>Hello, {name}</div>;
}
function ParentComponent() {
const [name, setName] = useState('太郎');
// nameが変更されるとChildComponentが再レンダリングされる
return <ChildComponent name={name} />;
}
3. 親コンポーネントが再レンダリングされたとき
これが最も見落としがちなポイントです。 Propsが変わっていなくても、親が再レンダリングされると子も再レンダリングされます。
function ChildComponent({ name }) {
console.log('ChildComponent rendered');
return <div>Hello, {name}</div>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
// countが更新されてParentが再レンダリングされると
// nameは変わっていないのにChildも再レンダリングされる
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ChildComponent name="太郎" />
</div>
);
}
この連鎖的な再レンダリングが、アプリケーションのパフォーマンスを低下させる主な原因となります。
レンダリング最適化の手法
ここからは、不要な再レンダリングを防ぐための具体的な手法を見ていきましょう。
最適化1: React.memoによるコンポーネントのメモ化
React.memoは、コンポーネントをメモ化してPropsが変わらない限り再レンダリングをスキップする機能です。
React.memoとは
React.memoは高階コンポーネント(HOC)で、コンポーネントをラップすることで使用します。
import { memo } from 'react';
const MyComponent = memo(function MyComponent({ name }) {
console.log('MyComponent rendered');
return <div>Hello, {name}</div>;
});
使用例とコード
先ほどのHeavyComponentをReact.memoで最適化してみましょう。
import { memo } from 'react';
// 重い計算処理を模擬する関数
function heavyCalculation(num) {
console.log('重い計算を実行中...');
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += num;
}
return result;
}
// React.memoでラップ
const HeavyComponent = memo(function HeavyComponent({ count }) {
console.log('HeavyComponent rendered');
const result = heavyCalculation(count);
return (
<div>
<h3>重いコンポーネント</h3>
<p>計算結果: {result}</p>
</div>
);
});
これで、count Propsが変更されない限り、親コンポーネントが再レンダリングされてもHeavyComponentは再レンダリングされなくなります。
どんなときに効果があるか
React.memoが効果を発揮するのは以下のようなケースです。
- 計算コストの高い処理を含むコンポーネント
- 大量のデータをレンダリングするリストコンポーネント
- 頻繁に親が再レンダリングされるが、自身のPropsはあまり変わらないコンポーネント
逆に、以下のような場合は使用しても効果が薄いです。
- Propsが頻繁に変わるコンポーネント
- レンダリングコストが非常に低いシンプルなコンポーネント
最適化2: useCallbackによる関数のメモ化
React.memoを使っても、関数をPropsとして渡している場合はうまく機能しないことがあります。なぜなら、JavaScriptでは関数も参照型のため、親コンポーネントが再レンダリングされるたびに新しい関数が生成されるからです。
useCallbackとは
useCallbackは、関数をメモ化して同じ参照を保持し続けるHookです。
import { useCallback } from 'react';
const memoizedCallback = useCallback(
() => {
// 関数の処理
},
[依存配列] // この値が変わったときだけ関数を再生成
);
React.memoと組み合わせた使用例
関数をPropsとして渡す場合の問題を見てみましょう。
import { useState, memo } from 'react';
// React.memoでラップされた子コンポーネント
const ChildComponent = memo(function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>クリック</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// この関数は毎回新しく生成される
const handleClick = () => {
console.log('ボタンがクリックされました');
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>カウント: {count}</p>
{/* textが変わるたびにhandleClickも新しく生成されるため
ChildComponentが再レンダリングされてしまう */}
<ChildComponent onClick={handleClick} />
</div>
);
}
この問題をuseCallbackで解決します。
import { useState, memo, useCallback } from 'react';
const ChildComponent = memo(function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>クリック</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// useCallbackで関数をメモ化
const handleClick = useCallback(() => {
console.log('ボタンがクリックされました');
setCount(c => c + 1); // 関数形式の更新を使用
}, []); // 依存配列が空なので、この関数は最初の1回だけ生成される
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>カウント: {count}</p>
{/* textが変わってもhandleClickの参照は変わらないため
ChildComponentは再レンダリングされない */}
<ChildComponent onClick={handleClick} />
</div>
);
}
依存配列の注意点
useCallbackの第2引数には、関数内で使用する変数を必ず指定する必要があります。
function ParentComponent() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
// ❌ 間違い: multiplierを依存配列に含めていない
const handleClick = useCallback(() => {
console.log(count * multiplier); // 古いmultiplierの値が使われ続ける
}, [count]);
// ✅ 正しい: 使用する変数を全て依存配列に含める
const handleClickCorrect = useCallback(() => {
console.log(count * multiplier);
}, [count, multiplier]);
return (
<div>
<button onClick={handleClickCorrect}>計算</button>
</div>
);
}
依存配列に含めるべき値:
- 関数内で参照するStateやProps
- 関数内で使用する他の関数
依存配列に含めなくてよい値:
- setStateなどのState更新関数(Reactが参照を保証)
- useRefで作成したref
補足: useMemoによる値のメモ化
useMemoとは
useMemoは、計算結果をメモ化して依存配列の値が変わらない限り再計算をスキップするHookです。
import { useMemo } from 'react';
const memoizedValue = useMemo(
() => {
// 重い計算処理
return 計算結果;
},
[依存配列] // この値が変わったときだけ再計算
);
useCallbackとの違い
useCallbackとuseMemoは似ていますが、メモ化する対象が異なります。
import { useCallback, useMemo } from 'react';
function MyComponent({ items }) {
// useCallback: 関数そのものをメモ化
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// useMemo: 関数の実行結果(値)をメモ化
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
return (
<div>
<p>合計: {total}</p>
<button onClick={handleClick}>クリック</button>
</div>
);
}
簡単に言うと:
- useCallback: 関数自体を保存(関数の参照を保持)
- useMemo: 関数を実行した結果を保存(計算結果を保持)
使用例
先ほどのHeavyComponentをuseMemoで最適化してみましょう。
import { useMemo } from 'react';
function heavyCalculation(num) {
console.log('重い計算を実行中...');
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += num;
}
return result;
}
function HeavyComponent({ count }) {
console.log('HeavyComponent rendered');
// useMemoで計算結果をメモ化
const result = useMemo(() => {
return heavyCalculation(count);
}, [count]); // countが変わったときだけ再計算
return (
<div>
<h3>重いコンポーネント</h3>
<p>計算結果: {result}</p>
</div>
);
}
この場合、コンポーネント自体は再レンダリングされますが、重い計算処理はcountが変わったときだけ実行されます。
useMemoが効果的なケース:
- フィルタリングやソートなど、配列の加工処理
- 複雑な計算処理
- 大きなオブジェクトや配列の生成
まとめ
最適化の使い分け
それぞれの最適化手法には適切な使いどころがあります。
| 手法 | 用途 | 効果 |
|---|---|---|
| React.memo | コンポーネントの再レンダリングを防ぐ | Propsが変わらなければコンポーネント全体をスキップ |
| useCallback | 関数の参照を保持する | 子コンポーネントへ渡す関数の参照を安定させる |
| useMemo | 計算結果を保持する | 重い計算処理の結果を再利用 |
パフォーマンス測定の重要性
最適化は常に必要というわけではありません。過度な最適化はコードの複雑性を増し、可読性を下げることもあります。
最適化を行う際は:
- まず測定する: React DevToolsのProfilerで実際のボトルネックを特定
- 必要な箇所だけ最適化: すべてのコンポーネントをmemoでラップする必要はない
- 効果を確認: 最適化後に本当に改善されたかを測定
// ❌ やりすぎな例
const TinyComponent = memo(function TinyComponent({ text }) {
return <span>{text}</span>;
});
// ✅ シンプルなコンポーネントはそのままでOK
function TinyComponent({ text }) {
return <span>{text}</span>;
}
最適化の基本原則:
- 測定してから最適化する
- 必要な箇所だけ最適化する
- シンプルさと可読性を保つ
React 19では将来的にReact Compilerによる自動最適化が進む予定ですが、これらの基本的な最適化手法を理解しておくことは、パフォーマンスの良いアプリケーションを作る上で重要です。