使い勝手の良いアプリケーションを開発する上で、速度は非常に重要な指標の一つです。
Google が Web サイトを評価する際にも、速度は重要な指標の一つとして扱われています。
また、遅いアプリケーションはユーザーに嫌われ、開発者としても、性能の低いコードを書きたくはありません。
そこで React で重要なのが、再レンダリングがどの程度パフォーマンスに影響を及ぼすかという点です。
このため、ReactではuseMemo
というHookが提供されています。
これは、時間とリソースを多く消費する関数の計算結果を保存(メモ化)し、再レンダリングの性能を向上させるためのものです。
useMemoとは?
useMemo
は、Reactの特別な機能で、複雑な計算を再度実行することなく、その結果を保存するためのものです。
この結果は再レンダリング間で保持され、その依存性の配列内の値が変更された場合にのみ再計算されます。
具体的なコード例を見てみましょう。
const a = 2;
const b = 3;
const sumOfSquares = useMemo(() => {
return a*a + b*b;
}, [a, b]);
console.log(sumOfSquares); // 出力: 13
この例では、a
とb
の二乗の合計を求める計算が行われ、その結果がuseMemo
によってメモ化(保存)されています。
そしてこの計算は、a
またはb
が変更されるまで再度実行されません。
なぜなら、変数が変更されない限り、計算結果は変わらないからです。
useMemoのメリット
useMemo
の最大のメリットは、再レンダリングのパフォーマンスを改善することにあります。
コストの高い関数が頻繁に呼び出されると、アプリケーション全体のパフォーマンスに影響を及ぼす可能性があります。
この影響は特に、頻繁に更新される大規模なアプリケーションで顕著です。
しかしuseMemo
を使用すれば、関数の結果をメモ化し、再レンダリング時に不必要な再計算を避けることができます。
その結果、アプリケーション全体のパフォーマンスが向上します。
また、少し難しい話にはなるのですがuseMemo
は参照の一貫性も保つことができます。
(参照の一貫性とは、あるオブジェクトが参照しているオブジェクトが変更された場合、そのオブジェクトも変更されることを意味します。)
これは、子コンポーネントにオブジェクトや配列などのpropsを渡す際に特に役立ちます。
なぜなら、そのpropsに依存するeffectやmemoを使用している子コンポーネントが、参照の不一致により不必要な再レンダリングを引き起こす可能性があるからです。
※参照の一貫性については少し難しいので、読み飛ばしても問題ありません。
useMemoの使い方
それでは、具体的なuseMemo
の使い方と、どういう場合にメリットがあるのかを見ていきましょう。
クリックするたびにカウントアップする、カウンターコンポーネントの作成について考えてみます。
import React, { useState, useMemo } from 'react';
// countの値に応じて計算量が増える関数(コストの高い関数)
const expensiveComputation = (counterValue) => {
let result = 0;
for (let i = 0; i < counterValue * 1000000; i++) {
result += i;
}
return result;
}
// 子コンポーネント
const Counter = () => {
const [count, setCount] = useState(0);
// ここで計算結果をメモ化
const computedValue = useMemo(() => {
return expensiveComputation(count);
}, [count]);
return (
<div>
<h1>数値 {count} の計算結果: {computedValue}</h1>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
// 親コンポーネント
const App = () => {
const [text, setText] = useState('');
return (
<div>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<Counter />
</div>
);
}
export default App;
上記のコードでは、親コンポーネントであるApp
のtext
状態が変わると、全体の再レンダリングが発生します。
これにより、子コンポーネントであるCounter
も同時に再レンダリングされます。
ただし、この再レンダリングが発生しても、Counter
のcount
状態が変わっていない場合、useMemo
によって前回の計算結果(computedValue
)が保持され、expensiveComputation
は再実行されません。
これにより、不要な計算を省くことができ、結果的にパフォーマンスの向上に寄与します。
この例からわかるように、useMemo
フックは、レンダリングのパフォーマンスを最適化するための強力な道具となることができます。
特に高コストな計算を含むコンポーネントの再レンダリングを効率的に制御する場面でその価値を発揮します。
useMemoの注意点とその対処法
メモ化された関数は、再レンダリング間で保持される
useMemo
でメモ化された関数は、再レンダリング間で保持されます。
そのため、メモ化された関数は、再レンダリング時に再計算されることはありません。
しかし、依存配列が適切に設定されていない場合、意図しない挙動が発生することがあります。
以下に、適切でないuseMemo
の使用例とその修正例を示します。
NGな例:
const App = () => {
// countの値が変更されるたびに再計算される
const [count, setCount] = useState(0);
// 依存配列が空なので、countが変更されても再計算されない
const doubleCount = useMemo(() => {
return count * 2;
}, []);
return (
<div>
<h1>{count} の2倍は {doubleCount} です</h1>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
上記の例では、依存配列が空なのでdoubleCount
は初回レンダリング時にしか再計算されません。
以下に修正例を示します。
修正例:
const App = () => {
const [count, setCount] = useState(0);
// countの値が変更されるたびに再計算される
const doubleCount = useMemo(() => {
return count * 2;
}, [count]); // 依存配列にcountを追加
return (
<div>
<h1>{count} の2倍は {doubleCount} です</h1>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
この修正例では、useMemo
の依存配列にcount
を追加しています。
これにより、count
の値が変更されたときにのみ、doubleCount
が再計算されます。
useMemo は必ずしもパフォーマンスを改善するわけではない
useMemo
を適切に使用するとパフォーマンスが改善することがありますが、不適切に使用するとパフォーマンスが逆に低下する可能性もあります。
これは、useMemo
自体がリソースを消費するからです。
useMemo
は計算結果をメモリに保持し、依存性配列を監視します。
そのため、複雑な計算を行っていない場合や頻繁に依存性配列の値が変更される場合などは、useMemo
を使用することでむしろパフォーマンスが低下する可能性があります。
また、useMemo
によってメモリ使用量が増加するという副作用も考慮に入れる必要があります。
つまり、useMemo
は重い計算をする関数や参照の一貫性を保つ必要がある場合など、必要に応じて慎重に使用すべきツールであると言えます。
「とりあえずuseMemo
を使っておけばパフォーマンスが改善される」というわけではないので、注意が必要です。
useMemo
とuseCallback
の違いは?
useMemo
とuseCallback
はともにメモ化(保存)を行うフックですが、それぞれ少し違った目的で使用されます。
useMemo
は計算結果をメモ化します。つまり、関数の実行結果を保存しておき、次回以降のレンダリング時にその保存された結果を再利用します。useCallback
は関数自体をメモ化します。つまり、特定の関数を保存しておき、その関数が変更されない限り同じ関数を再利用します。
下記の例でそれぞれの使い方を見てみましょう。
useMemo
の例:
const ExpensiveComponent = () => {
const [count, setCount] = useState(0);
// countの値が変更されるたびに再計算される
const expensiveValue = useMemo(() => {
let i = 0;
while (i < 1000000000) i++; // 高コストな計算
return count * 2; // 計算結果を返す
}, [count]); // countが変更されたときだけ再計算する
return (
<div>
<h1>{count} の2倍は {expensiveValue} です</h1>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
useCallback
の例:
const CallbackComponent = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => { // increment関数をメモ化
setCount(count => count + 1);
}, []); // 依存配列が空なので、関数は一度だけ作成される
return (
<div>
<h1>カウント: {count}</h1>
<button onClick={increment}>増加</button>
</div>
);
}
上記のuseCallback
の例では、increment
関数自体がメモ化されています。
useMemo
とuseCallback
の両方とも、再計算や再作成を回避することでパフォーマンスの改善に貢献できますが、適切な状況と使用方法を理解することが重要です。
まとめ
ReactのuseMemo
フックは、高コストの計算結果をメモ化し、不必要な再計算を避けることでパフォーマンスを向上させることができます。
しかし、useMemo
は必ずしもパフォーマンスを改善するわけではなく、不適切な使用は逆にパフォーマンスを低下させたり、メモリ使用量を増加させる可能性があります。
また、useMemo
と類似のuseCallback
との違いを理解することも重要で、これらのフックを適切に使用することでReactアプリケーションのパフォーマンスを最大化することができます。
useMemo
を正しく理解し、適切に使用することで、さらに効率的でパフォーマンスが高いReactアプリを開発しましょう!
React を効率的に身につける勉強法は?
React には興味があっても、プログラミング学習をはじめたばかりであれば、以下のような疑問を持つかもしれません。
- Reactエンジニアとして活躍するために何をすればいい?
- 何から勉強すればいいんだろう?
- 絶対に転職を成功させるには?
React は習得が比較的難しいため、勉強法の選び方を間違うと時間・労力・お金が無駄になってしまいます…。
そのため、こちらの記事を参考に、ご自身に最適な学習方法を選んでみませんか?