はじめに
ReactでuseMemoやuseCallbackというhookを聞きますが、メモ化する関数としか認識できていないので、具体的な実装や違いなどについて理解していけたらなと思って書いていきます。
useMemoやuseCallbackを検索すると多くの方が記事を書いていているので、具体的な実装例など詳しくはそちらにお任せして、この記事では概要というか要点だけをまとめていこうかと思います。
そもそもメモ化とは?という方はこちらにまとめたので確認してみてください。
パフォーマンスチューニング
まずuseMemoなどを理解する前の前提として、パフォーマンスチューニングについて軽く触れたいと思います。
パフォーマンスチューニングは広義に捉えると、「システムの処理性能を高めるために、システムの動作を最適化すること」です。
Reactにおけるパフォーマンスチューニングとは、「無駄な計算や再レンダリングの負荷を減らして動作を最適化すること」と考えていいかなと思います。
Reactはprops・stateが更新されるとそのコンポーネント配下の全コンポーネントが再レンダリングされます。
そこで、useMemoなどを使用して再レンダリングさせないタイミングをホワイトリスト形式でコンポーネントごとに定義していきます。
もっと具体的に詳しく知りたい方はこちらの記事を参考にしてみてください。
メモ化する3つの方法
メモ化するものとして3つ存在します。
メモ化する対象は?
React.memo, useCallback, useMemo はそれぞれメモ化する対象が異なります。
またuseCallback, useMemoはhookですが、React.memoに関してはメモ化したコンポーネントを返すHOC(higher-order component)となっています。
名前 | メモ化する対象 | 種類 |
---|---|---|
React.memo | コンポーネント | HOC |
useCallback | コールバック関数 | hook |
useMemo | 計算結果の値 | hook |
メモ化の歴史
それぞれを理解する前に、パフォーマンスチューニングの歴史を知るとより理解できると思うので、ここで軽く紹介します。
クラスコンポーネントではshouldComponentUpdate
というメソッドをオーバーライドして、引数にpropsを指定し、新旧を比較することで、再レンダリングするかを制御していました。
しかし、propsが増えるたびに比較する処理を記述していく必要があったため、それを解決するために、自動で比較してくれるPureComponent
というコンポーネントが誕生しました。
こうしてクラスコンポーネントでは比較処理コストや再レンダリングコストの大小によって2つを使い分けて、パフォーマンスチューニングを行っていたようです。
ただ、関数コンポーネントではそういった機能を用意していなかったのでチューニングができず再レンダリングされていました。
しかし、React v16.6からReact.memoが追加され、関数コンポーネントでもshouldComponentUpdate
と同様の機能が使えるようになりました。
そして、React v16.8からHooksが追加され、useMemoとuseCallbackによってさらにパフォーマンスチューニングがしやすくなりました。
参考
React.memo
React.memoは前述のように、コンポーネントをメモ化するHOCで、propsを受け取り新旧を比較して再レンダリングするかを制御します。
しかしコールバック関数をpropsで受け取った時は再レンダリングされてしまいます。
関数はコンポーネントが再レンダリングされる度に新しいオブジェクトとして生成されるので、propsとして受け取ると等価として評価されずに再レンダリングされてしまうのです。
そこで、同一の関数オブジェクトとするために、useCallbackを使用して、propsに渡す関数をメモ化することでこの現象を回避することができます。
useCallback
useCallbackはメモ化したコールバック関数を返します。
React.memoとの併用が主な用途で、単体ではあまり使用しないらしいです。 (useCallbackを実行するコストより関数を再生成するコストの方が大きくなる局面がほぼないらしい * 後述します)
useCallbackの注意点として避けるべきことが2点あります。
- React.memo でメモ化をしていないコンポーネントに useCallback でメモ化をしたコールバック関数を渡す
- メモ化をしていないコンポーネントに、メモ化した関数を渡しても再レンダリングされてしまいます。
- useCallback でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する
- 関数自体は実行できますが、再レンダリングさせないということはできません。
useMemo
useMemoは計算結果の値をメモ化します。
不要な再計算を避けることでパフォーマンスを上げます。
なお、useMemoはレンダリング結果もメモ化できるので、React.memoと同じようにコンポーネントの再レンダリングを制御することもできます。
具体例
具体例は自分がとてもわかりやすいと思った記事のリンクを貼っておきます。
こちらを参考にしてみてください。
- React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする – Qiita
- React レンダリング最適化(useMemo, useCallback, React.Memo)
useCallbackの単体での利用について
useCallbackは前述のように、React.memoとの併用が主な用途で、単体ではあまり使用しないらしいです。
理由としては「関数を再生成するコスト > useCallbackの実行コスト単体」となる局面がほぼないとのことです。
しかし、useCallback単体で使用している例を見かけたことがあるので、ここで紹介しておきます。
const [users, setUsers] = useState<User[]>([])
const getUsers = useCallback(async () => {
try {
const response = await axios.get<{ users: User[] }>('/api/users')
if (isMountedRef.current) {
setUsers(response.data.users)
}
} catch (err) {
console.error(err)
}
}, [isMountedRef])
useEffect(() => {
getUsers()
}, [getUsers])
useCallbackを使用すると、関数の再生成をしないため、中の処理を何回も実行しないで済むようになっています。
このように一応単体での使用例もあるみたいです。
React.memoとuseMemoの違い
React.memoはコンポーネントをメモ化します。
useMemoは計算結果の値をメモ化しますが、コンポーネントのレンダリング結果のメモ化もできるので、React.memoと同じように実装することができます。
そこで、これらの違いとは?どういう使い分け?という疑問が浮かんだのですが、具体的な例があまり出てこなかったです。
違いと言えばHOCとhookとなるので、用途によって違うということでしょうか。
参考になりそうな記事も貼っておきます。
コメントなどで意見や具体例をいただけると助かります。
React.memo構文の問題の代わりにuseMemoを使用する
さいごに
Reactにおけるパフォーマンスチューニングは、3つを使用することで実装できます。
名前 | メモ化する対象 | 種類 |
---|---|---|
React.memo | コンポーネント | HOC |
useCallback | コールバック関数 | hook |
useMemo | 計算結果の値 | hook |
具体的な実装例はあえて書かずに文章だけで説明をしたので、理解ができないところは参考記事をよくみてみてください。
今後Reactの実装でパフォーマンスを考えた実装をするために、状況に応じてそれぞれ使い分けていければと思います。