そろそろしっかりと理解しておきたいと思い、React.memo, useCallback, useMemoについて改めて勉強しました。
こちらの記事では、メモ化の概念や詳しい作用機序については解説しません(公式ドキュメントや素晴らしい記事が既にたくさんあります)。
この記事は、React.memoやuseCallback,useMemo などのパフォーマンス最適化の為の機能を使用して、どうコードを書けばどう動くようになる(どのように差が出る)のかということを最短時間で理解できるように書いてみたいと思います。
使い所
React.memo, useCallback, useMemoなどの機能は全て、コンポーネントのレンダリングを最適化するパフォーマンスチューニングの為の機能です。
これらの機能の使用による効果が明らかに得られるのは、処理の重たい子コンポーネントによって、コンポーネントのレンダリングパフォーマンスに影響が出ているときです。
シンプルで極端な例で、このシチュエーションを確認してみましょう。
そしてその次にこれらの最適化の機能を使って、パフォーマンスの改善を行い効果を確認していきましょう。
実践1 シチュエーションの確認
今回の学習用のメチャ重い子コンポーネントをもつアプリケーションはこちらです。
import { useState, useCallback } from 'react';
import Child from './Child';
function App() {
const [count, setCount] = useState(0);
const [inputState, setInputState] = useState('');
const addCountHandler = () => {
setCount(count + 1);
};
return (
<div className='App'>
<div>Count: {count}</div>
<input type='text' onChange={(e) => setInputState(e.target.value)} />
<Child addCountHandler={addCountHandler} />
</div>
);
}
export default App;
function Child({ addCountHandler }) {
console.log('rendered Child');
const heavyOperationWithoutMemo = () => {
let output = 0;
for (let i = 0; i < 500000000; i++) {
output++;
}
return output;
};
return (
<div className='child'>
<h2>Child Component</h2>
<p>output : {heavyOperationWithoutMemo()}</p>
<button onClick={addCountHandler}>+1 from Child</button>
</div>
);
}
export default memo(Child);
状況の確認
アプリケーションはcountというボタンが押されれば+1ずつ増えていく数字のstateと、inputStateというinput type=text の値を保持する二つのstateを持っている。
子コンポーネントに、countのstateに+1する処理を記述した関数addCountHandlerをpropsとして渡して子コンポーネントのボタンのイベントハンドラーに登録している。
子コンポーネントはレンダリングのたびにループが5億回マワる激重関数を持っている為、レンダリングがクソ重たい。
Qiita埋め込み用動画1 激重コンポーネント pic.twitter.com/tBCmUFOufn
— 70ki8suda (@70ki8suda) January 31, 2022
こちらの動画では伝わりにくいかもしれないが、+1ボタンを押してstateに反映されるのに体感.5秒くらいかかっていて、inputの値を変更したときもレンダリングにかなりのラグがある。
今回処理が重くなっている原因は、子コンポーネントのレンダリングのたびに走るループ処理である。
子コンポーネントには直接関係のない親のinputStateの値が変更されるたび、子コンポーネントも再レンダリングされていて、パフォーマンスに影響を及ぼしている。
まずは、子コンポーネントを親コンポーネントの関係のない状態から切り離して、必要のないときは、再レンダリングされないようにしよう。
React.memoで子コンポーネントの再レンダリングを親から切り離す
まず、React.memoを使って、子コンポーネントそのものをメモ化し、プロップスが同じときは再レンダリングされないようにする。
やることは簡単。
import { memo } from 'react';
React.memoをインポートして
export default memo(Child);
Childコンポーネントに処理を適用するだけ。
これで、子コンポーネントがメモ化された。
しかし、今回のケースはこれだけではinputStateの変更の度に、処理が重くなる問題が解決していない。
原因: ChildコンポーネントはaddCountHandler関数を親からpropsとして受け取っているが、このaddCountHandler関数が親コンポーネントの再レンダリングの度に再定義される為、親のinputStateの変更のたびにpropsのaddCountHandler関数が再定義され、メモ化したにも関わらずChildコンポーネントの再レンダリングが走っている
useCallbackでメモ化したコンポーネントに渡す関数をメモ化する
Childコンポーネントのレンダリングを親のinputStateの変更から独立させるには、propsで渡されているaddCountHandler関数をメモ化すればよい。
このときに使うのがuseCallbackフックである。
const addCountHandler = useCallback(() => {
setCount(count + 1);
}, [count]);
この処理により、addCountHandler関数はcountのstateが変更されたときのみ再定義されるようになった(コンポーネントの他のstateの変更から独立した)。
OK!これでinputStateとChildコンポーネントの値が独立した!
パフォーマンスの改善を確認してみよう!
Qiita埋め込み用 親のinputStateの更新から子コンポーネントのレンダリングを切り離すことに成功 pic.twitter.com/d08Z819PGr
— 70ki8suda (@70ki8suda) January 31, 2022
動画で伝わるかわからないですが、前はテキストインプットに入力する度に子コンポーネントの再レンダリングが走っていたので処理が超重たかったのですが、これで、どうやら親のテキストインプットが更新されても子コンポーネントが再レンダリングされないように状態を切り離すことに成功したようで、とてもサクサクになりました!!
パチパチ!!
しかし、まだパフォーマンスを解決したい問題が一つ残っています。
ボタンをクリックしてcountの値が変わりChildコンポーネントの再レンダリングの度に、5億回のループ処理が走るため、ボタンの処理は相変わらず重たい…
Childコンポーネントの5億回のループ処理をメモ化したい!
useMemoで不要な再計算をスキップ
const heavyOperation = useMemo(() => {
let output = 0;
for (let i = 0; i < 500000000; i++) {
output++;
}
return output;
}, []);
ChildコンポーネントのheavyOperation関数をメモ化した。
これにより、再レンダリング時は再計算をスキップしてキャッシュの値を利用できるようになった!
これで、ボタンの挙動もサクサクになるか!?
Qiita埋め込み用 React.memo,useCallback,useMemoでサクサクに pic.twitter.com/fQ67v3C5aG
— 70ki8suda (@70ki8suda) January 31, 2022
やったー!
ボタンの挙動もサクサクになりました!(動画でわかりにくいかもしれませんが)
これにてパフォーマンスチューニング完了です!(* ´꒳`)ノ"お疲れ様でした♪
最終的なコード
import { useState, useCallback } from 'react';
import Child from './Child';
function App() {
const [count, setCount] = useState(0);
const [inputState, setInputState] = useState('');
const addCountHandler = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div className='App'>
<div>Count: {count}</div>
<input type='text' onChange={(e) => setInputState(e.target.value)} />
<Child addCountHandler={addCountHandler} />
</div>
);
}
export default App;
import { memo, useMemo } from 'react';
function Child({ addCountHandler }) {
const heavyOperation = useMemo(() => {
let output = 0;
for (let i = 0; i < 500000000; i++) {
output++;
}
return output;
}, []);
return (
<div className='child'>
<h2>Child Component</h2>
<p>output : {heavyOperation}</p>
<button onClick={addCountHandler}>+1 from Child</button>
</div>
);
}
export default memo(Child);
今回、5分でReact.memo, useCallback, useMemoの使い所を学ぶ!というコンセプトで記事を書かせていたきましたが、コードの方をgithubにアップしました。遅い処理のコードもコメントアウトで残してあるので、手元の環境でぜひ、さわって挙動を確かめていただけると、安心感が得られると思います。
https://github.com/70ki8suda/learn_useMemo_useCallback
なお、今回の記事は、私が学習するときに見た海外のyoutubeの動画をより簡潔に簡略して文字起こししたようなものになっている為、英語に強い方、動画でサクッと学びたい方は
をチェックしてください!
最後までお読みいただき、ありがとうございました!