1. はじめに
今回はReactのパフォーマンス最適化について学びました。
具体的には、メモ化の3つの手法であるuseMemo・React.memo・useCallbackを学び、それぞれの使い分けを整理しました。
2. メモ化とは何か
Reactにおけるメモ化の全体像を整理するために、まず3つの手法の違いを確認しておきます。
| 手法 | メモ化する対象 | 主な用途 |
|---|---|---|
useMemo |
計算結果の値 | 重い計算を必要なときだけ実行したい場面 |
React.memo |
コンポーネント全体 | propsが変わらない子コンポーネントの再レンダリングを防ぎたい場面 |
useCallback |
関数 |
React.memoでメモ化した子コンポーネントに関数をpropsとして渡す場面 |
メモ化とは、一度実行した計算結果を記憶しておき、同じ計算が必要になったときに記憶した結果を再利用する仕組みのようです。
これら3つを使い分けることで、不要な再計算や再レンダリングを防ぎ、アプリケーションのパフォーマンスを改善できます。
2.1 なぜパフォーマンスが悪化するのか
Reactでは、いずれかのstateが変更されるとコンポーネント全体が再レンダリングされ、コンポーネント内のすべてのコードが再実行されます。
そのため、計算に無関係なstateが変更されても、重い計算処理が毎回実行されてしまいます。
この不要な計算の再実行が、アプリケーションが遅くなる原因のようです。
3. useMemoで計算結果をメモ化する
useMemoを使って、重い計算処理をメモ化する方法を学びました。
useMemoは、時間のかかる計算処理の結果を記憶しておくReactのフックで、依存配列に指定した値が変化したときのみ計算を再実行します。
3.1 メモ化前のコード(問題の確認)
まず、重い計算処理が含まれたコードを確認します。
countとotherCountという2つのカウンターを持ち、どちらのボタンをクリックしても重い計算が走ってしまう問題を体感できるコードです。
import { useState } from 'react';
import './App.css';
function App() {
// 2つのカウンターのstateを定義
const [count, setCount] = useState(0);
const [otherCount, setOtherCount] = useState(0);
// 重い計算処理(10億回ループ)
console.log('計算が実行されました');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += count * i;
}
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<p>その他のカウント: {otherCount}</p>
<button onClick={() => setOtherCount(otherCount + 1)}>+</button>
<p>計算結果: {result}</p>
</div>
);
}
export default App;
otherCount自体は重い計算に無関係なのに、otherCountのボタンをクリックするだけで重い計算が実行されてしまいます。
これが再レンダリングによる不要な計算の問題のようです。
3.2 useMemoで改善したコード
次に、useMemoを使って重い計算処理をメモ化したコードです。
// useMemoをインポートに追加
import { useState, useMemo } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const [otherCount, setOtherCount] = useState(0);
// useMemoで計算結果をメモ化
console.log('計算が実行されました');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += count * i;
}
// countが変わったときのみ再計算
const result = useMemo(() => {
console.log('計算が実行されました');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += count * i;
}
return result;
}, [count]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<p>その他のカウント: {otherCount}</p>
<button onClick={() => setOtherCount(otherCount + 1)}>+</button>
<p>計算結果: {result}</p>
</div>
);
}
export default App;
useMemoの基本構文は以下のようになります。
// useMemoの基本構文
const メモ化された値 = useMemo(() => {
// 計算処理
return 計算結果;
}, [依存する値]);
第1引数には計算を行う関数を渡し、第2引数には依存配列を渡します。
依存配列に入れた値が変化した場合のみ計算が再実行されます。
今回は依存配列に[count]を指定しているため、countが変わったときのみ計算が再実行されます。
これにより、otherCountのボタンをクリックしても計算がスキップされ、遅延がなくなります。
4. React.memoでコンポーネント全体をメモ化する
React.memoは、コンポーネント全体をメモ化する機能です。
親コンポーネントが再レンダリングされても、propsに変化がなければ子コンポーネントの再レンダリングを防ぐことができます。
4.1 子コンポーネントが不要に再レンダリングされる問題
React DevToolsの「Highlight updates when components render」を有効にすると、再レンダリングされたコンポーネントが色付きの枠線でハイライトされます。
頻繁に再レンダリングされる部分は赤色、頻度が低い部分は緑色で表示されます。
まず、メモ化していない状態のコードです。
propsを何も受け取らないChildコンポーネントを追加して、再レンダリングの動作を確認します。
import { useState, useMemo } from 'react';
import './App.css';
// 子コンポーネントを定義
function Child() {
return <p>子コンポーネント</p>;
}
function App() {
const [count, setCount] = useState(0);
const [otherCount, setOtherCount] = useState(0);
const result = useMemo(() => {
console.log('計算が実行されました');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += count * i;
}
return result;
}, [count]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<p>その他のカウント: {otherCount}</p>
<button onClick={() => setOtherCount(otherCount + 1)}>+</button>
<p>計算結果: {result}</p>
{/* 子コンポーネントを使用 */}
<Child />
</div>
);
}
export default App;
Childにpropsは渡していないのに、親のcountが変更されるたびにChildも再レンダリングされてしまうことが確認できます。
4.2 React.memoで子コンポーネントをメモ化したコード
React.memoを使ってChildコンポーネントをメモ化します。
// memoをインポート
import { useState, memo } from 'react';
import './App.css';
// React.memoでコンポーネントをメモ化
const Child = memo(function Child() {
return <p>子コンポーネント</p>;
});
React.memoの基本構文は以下のようになります。
// React.memoの基本構文
const メモ化されたコンポーネント = memo(function コンポーネント名(props) {
// コンポーネントの内容
});
React.memoで囲まれたコンポーネントは、propsが前回と同じ値であれば再レンダリングされません。
Reactはpropsの値を前回と比較し、すべて同じであれば前回のレンダリング結果を再利用します。
5. useCallbackで関数をメモ化する
useCallbackは、関数の再作成を防ぐReactのフックです。
React.memoだけでは最適化できないケースを解決するために使います。
5.1 関数をpropsで渡したときの問題
React.memoでメモ化した子コンポーネントに関数をpropsとして渡すと、最適化が崩れてしまうことがあります。
以下のコードでその問題を確認します。
import { useState, memo } from 'react';
import './App.css';
// 子コンポーネントにonClick propsを受け取るよう変更
const Child = memo(function Child({ onClick }) {
return <p onClick={onClick}>子コンポーネント</p>;
});
function App() {
const [count, setCount] = useState(0);
// クリック時の処理を定義
const handleClick = () => {
console.log('クリックされました');
};
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
{/* 関数をpropsとして渡す */}
<Child onClick={handleClick} />
</div>
);
}
export default App;
関数の内容は同じなのに、親コンポーネントが再レンダリングされるたびにhandleClick関数が新しく作成されます。
Reactは関数の内容ではなく参照を比較するため、同じ処理内容でも異なる関数として判断されてしまいます。
その結果、React.memoによるメモ化が無効になってしまうようです。
5.2 useCallbackで関数をメモ化したコード
useCallbackを使ってhandleClick関数をメモ化します。
// useCallbackをインポートに追加
import { useState, memo, useCallback } from 'react';
import './App.css';
const Child = memo(function Child({ onClick }) {
return <p onClick={onClick}>子コンポーネント</p>;
});
function App() {
const [count, setCount] = useState(0);
// useCallbackで関数をメモ化
const handleClick = useCallback(() => {
console.log('クリックされました');
// 空配列のため、関数は初回のみ作成される
}, []);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<Child onClick={handleClick} />
</div>
);
}
export default App;
useCallbackは第1引数にメモ化したい関数、第2引数に「どの値が変化したときに関数を再作成するか」を指定する配列を受け取ります。
依存配列が空配列[]のため、関数は初回のみ作成され、その後は同じ関数が再利用されます。
これにより、React.memoとuseCallbackを組み合わせることで、関数をpropsとして渡す場合でも効率的な最適化が可能になります。
6. メモ化を使わない方が良い場面
メモ化の3手法を学びましたが、すべての処理にメモ化が適しているわけではないようです。
メモ化には依存配列の比較処理やメモ化された値を保持するメモリなど、メモ化自体にもコストがかかります。
以下のような場面では、メモ化の効果が薄いか、むしろコストの方が高くなってしまうようです。
-
簡単で速い処理:数値の足し算(
1 + 2)や短い文字列の結合など、瞬時に終わる処理 -
毎回違う結果になる処理:現在時刻(
new Date())やランダムな数値(Math.random())など、実行するたびに必ず異なる結果が返される処理 -
propsが頻繁に変わる処理:
React.memoでメモ化していても、propsが頻繁に更新されるコンポーネントでは再レンダリングが頻発し、メモ化の効果が得られない処理
まずメモ化を使わずにアプリケーションを作り、実際に遅くなってからメモ化を使うことを検討するのが良いようです。
まとめ
今回はReactにおけるメモ化の3手法(useMemo・React.memo・useCallback)を学びました。
「なぜ再レンダリングが起きるのか」という仕組みを理解してから最適化の手法を学ぶことで、それぞれの用途がよく整理できた気がします。
今回の気づき
Reactの再レンダリングはいずれかのstateが変更されるとコンポーネント全体で発生するため、無関係な処理まで再実行されてしまうことがわかりました。
React.memoだけでは解決できないケースがあり、関数をpropsとして渡す場合はuseCallbackと組み合わせる必要があるという点が特に印象に残っています。
メモ化はあくまでパフォーマンスが問題になってから検討するもので、最初から使いすぎないことが大切だと感じました。
ハマりやすいポイント
-
React.memoを使っても、関数をpropsで渡すと再レンダリングが発生してしまう(Reactは関数の参照を比較するため) -
useCallbackの依存配列を空にすると関数は初回しか作成されないが、依存配列の指定を間違えると期待通りに動作しない - メモ化自体にもコストがかかるため、シンプルな処理にまで使うと逆効果になる