こんにちは。 AG-Boost事業本部の大塚です。
最近、自社ツールのもっさり感を解消するためパフォーマンス改善を行いました。
フロントでは、キャッシュ化による不要な再計算・再レンダリングを抑えることが重要です。
この記事では、メモ化で頻出のReact.memo, useMemo, useCallback等について、使い方と使い所をまとめてみました。 先月遊んでいたTypescriptの型パズルに倣って いくつか問題を用意したので、パズル感覚でお楽しみいただければと思います。
1. 基礎知識
1-1. レンダリングとは
レンダリングとは、以下の2stepで画面を描画することである。
1, componentを再計算して仮想DOMを作る
2, 仮想DOMに前回と差分が生じた場合、差分だけをリアルDOMに反映させる
1-2. 再レンダリングのタイミング
再レンダリングされる条件は主に2つ
- stateが更新された時
- 親コンポーネントが再レンダリングされた時
2. メモ化問題4選
2-1. 不要な再レンダリングを止めよう
コード: https://codepen.io/hihsswbw-the-reactor/pen/QWzmKOv
構成:
<App />
配下に<child1>
と<child2>
がある。- ボタンを押すとstateが変わり
が再レンダリングされる(#1) - 親と共に、二つの
も再レンダリングされる(#2)
問題:
<Child2>
は状態が不変なので、再レンダリングする必要がない。
React.memoを使って、ボタンを押しても<Child2>
が再レンダリングされないようにしよう。
答え: child2をReact.memoでラップする。
const Child2 = React.memo((props) => { const { text } = props; console.error(text); return ( <div> child2 </div> ); });
React.memoでラップしたコンポーネントは、レンダリング前後でpropsに変更が無い場合(shallow比較で判定)、再レンダリングをスキップできる。
親のレンダリングがChild2に伝播する(#2)...ここがReact.memoにより免除される。
変更前と変更後のprops比較処理が入る分、結果的に再レンダリングが必要と判断される場合は、逆にパフォーマンスダウンとなります。
再レンダリングの必要性が高いほどReact.memoの有効性は落ちます。
また、使う際は不要なpropsを渡していないかチェックするといいです。
(※) 第二引数で比較用の関数を渡すこともできます。 メンテナンスが難しくなので、使わない方が安全な気がします... ja.legacy.reactjs.org
2-2. 不要な再レンダリングを止めようver2
コード: https://codepen.io/hihsswbw-the-reactor/pen/dywmqQx
構成:
- 2-1の続き
<Child2>
に渡すpropsを変えた所、React.memoでラップしているのに、ボタンを押すと再レンダリングされる状態に戻ってしまった。
問題: useMemo, useCallbackを使って、ボタンを押しても<Child2>
が再レンダリングされないようにしよう。
答え:
const map = useMemo(() => return { name: 'child2', }, []); const createText = useCallback(() => { return 'child2 rendered'; }, []);
関数やオブジェクトは再定義される度に参照が変わる。
そのため、Child2のpropsは「変更された」と判断され再レンダリングが起きる。
useMemoはオブジェクトを、useCallbackは関数をラップしてキャッシュ化することができる。
キャッシュ化して参照が変わらなければ、propsの変更を検知できず、React.memoにより再レンダリングが防止される。
必要に応じて再計算したい場合は、第二引数を利用する。
レンダリング前後で引数の値に変更があれば、再計算されるようになる。
useMemo – React
2-3. 処理が重い変数の再計算を止めよう
コード: https://codepen.io/hihsswbw-the-reactor/pen/LYMdJaK
構成:
- ボタンを押すと
<App>
が再レンダリングされて、sum
が再計算される。 calcSum()
は、計算量は多いが常に同じ値を返す。- 再レンダリングにかかった時間は、ブラウザのコンソールで確認できます。
問題:
sum
をキャッシュ化して、ボタンを押した時のもっさり感を無くそう。
答え:
const sum = useMemo(() => calcSum(), []);
初回レンダリングで計算した値を使い回すことで、初回レンダリング以降はsum
の計算を省くことができます。
ログを見れば一目瞭然で違いが分かりますね。
前) タイム: 292.240966796875 ms
後) タイム: 1.418701171875 ms
2-4. 再レンダリングが必要ない値はuseRefで管理しよう
コード: https://codepen.io/hihsswbw-the-reactor/pen/dywemLm?editors=1011
構成:
- inputの入力値がstateに保持される
- 入力の度にstateが変わるので、
<App>
が再レンダリングされる
問題: stateではなくrefを使って入力値を管理し、入力中の再レンダリングを防ごう
答え:
const [inputValue, setInputValue] = useState(''); ⇩ const inputRef = useRef(''); value={inputValue} onChange={(e) => { setInputValue(e.target.value); }} ⇩ ref={inputRef} onClick={() => { setOutputValue(inputValue); }} ⇩ onClick={() => { setOutputValue(inputRef.current.value); }}
inputの入力値はinputRef.current.value
に保持されます。
refは常に同じ参照を持つので、再レンダリングせずに入力値をcurrent.valueに保持できます。
スクロール位置などの、更新頻度が高い値を扱う際に便利です。
Referencing Values with Refs – React
今回はボタンを押して表示を更新する形式ですが、リアルタイムで表示を変える場合、入力の度に再レンダリングが必要となります。 その場合はrefを使うよりも、debounceを使って処理を間引く等のやり方が有効だと思います。
最後に
今回紹介したメモ化については、解説記事が沢山あります。
この問題を解きながら、他の記事と合わせて少しでも理解の助けになればと思いますm(--)m