皆さんはAIでコーディングしていますか?
今や手でコーディングする機会はぐんと減り、人はAIが書いたコードをレビューする作業をするようになってきました。(もはやレビューもAIが行うという形も増えつつありますが。。。)
かくいう自分も(全てではないですが)AIコーディングで開発をしています。Next.js(React)でフロント開発をしててAIが書いたコードをレビューしているときにふと毎回思っていたことが
関数を作成すると毎回useCallbackでラップしてるな。
パフォーマンス観点で使用していることはなんとなく理解しているのですが、なぜ使用するのかという問いに対しては正確に答えることができません。
なので今回はあらためてuseMemoとuseCallbackについて学んでみました。
useMemo, useCallbackとは
useMemo
値をキャッシュし再利用するHook
useCallback
関数をキャッシュし再利用するHook
どちらもキャッシュを行うことを目的とし、無駄な再レンダリングを抑制しパフォーマンス改善に利用します。 Reactは主に以下のイベントでレンダリングを行います。
- state が変わったとき(setState / useState)
- props が変わったとき(親から新しい値が渡る)
- 親コンポーネントが再レンダリングされたとき
つまり、この時点でなんとなくイメージできますが
1つの値の変化がコンポーネント全体に影響するのです
なのでUIの変化が激しい昨今のwebアプリケーションではパフォーマンス観点にも注意する必要があります。
ここからuseMemo,useCallbackについてまとめていきます。
- 概要
- 使い方
- 深掘り
の順にまとめます。なんとなく理解したい場合は2までで大丈夫です。(現状、自分も2までは理解してる) 今回はさらに、なぜその書き方になるのか、どのような場面で必要になるのか、を理解できるところまで深掘りたいと思います。AIが書いたコードを適切にレビューできるようになりましょう。
useMemo
概要
useMemoは値をキャッシュします。 値を算出するために必要な処理が重いなどの理由で、値とは関係のないレンダリングでは処理を無視したいなどの状況では、その算出処理をuseMemoでラップすることで値をメモすることができます。
使い方
const result = () => {
return heavyCalculation(a, b)
}
このようにheavyCalculation(何かしらの重い処理)で計算された値がresultに格納されるとします。レンダリングのたびにheavyCalculationが走るのでパフォーマンスに問題が起きそうです。
しかし、aとbの値が変わらなければresultの値は変わりません。なのでa,bどちらかの値が変化した場合に処理が走るようにresultの値をメモしましょう。
const result = useMemo(() => {
return heavyCalculation(a, b)
}, [a, b])
useMemo(func, deps)
func: メモ化する値を計算する関数
deps: 依存する変数(配列)
深掘り
第一引数について
useMemoの第一引数には関数を記述します。キャッシュする値を算出するロジックのことですね。
上記の例ではアロー関数で記述していますが、アロー関数でなくても記述可能です。
// アロー関数以外の書き方
// 通常のfunction宣言
useMemo(function () {
return heavy(a, b)
}, [a, b])
// 依存関数の参照
function calc() {
return heavy(a, b)
}
const result = useMemo(calc, [a, b])
引数を指定してはいけない
ここで注意しなくてはいけないのが、関数に引数を指定しないことです(クロージャで宣言する)。
理由はReactが、関数を引数なしでコールするからです。
callback()
でコールするので、引数を指定してもundefinedになります。
また、これもふと思ったのですが、useMemoは値をキャッシュするなら以下のような書き方ではダメなのか
const value = func(a,b)
const memoValue = useMemo(value, [a,b])
もうすでに理解されていると思いますが、もちろんこれも引数を持つ関数を指定しているのでダメです。
a,bから計算されるvalueの値をキャッシュしたいのでしょうが、memoValueとしてキャッシュする前にfuncを実行しています。そもそもネックになっている重い処理を実行済みの状態でメモ化しようとしているので意味がありません。
まとめると以下のようになります。
第一引数には、必要なときだけ(第二引数の配列に指定した変数の値が変更されたときに)実行する関数を指定する
useMemoは値をキャッシュするために必要な計算ロジックをメモするものなんですね。
useCallback
概要
useCallbackは関数をキャッシュします。useMemoと同じく再レンダリング時の無駄な処理を抑制することを目的としています。
使い方
const memoFunc = useCallback((a, b) => {
// なんらかの処理
}, [a, b])
useMemo(func, deps)
func: メモ化する関数
deps: 依存する変数(配列)
useCallbackの書き方もuseMemoと同じで第一引数にメモ化したい関数、第二引数に依存配列を指定します。useCallbackは関数をキャッシュするためのフックなので、第一引数にはもちろん任意の関数を指定することができます。
深掘り
関数のキャッシュとは
そもそも「関数をキャッシュする」ということがどういうことか整理します。
関数のキャッシュは、「関数の生成」をキャッシュするわけでなありません。関数の生成に関してはどんなに大きな関数であっても問題になる程コストがかかるものではありません。
関数をキャッシュするとは、正しくは「関数の参照」をキャッシュするということです。
const func1 = () => console.log("function")
const func2 = () => console.log("function")
func1 === func2 // false
上の例でわかるように、func1とfunc2は中身が同じでも参照先が異なるので実体としては異なる関数です。これと同じ理屈で、
function Parent() {
const handleClick = () => console.log("click")
return <Child onClick={handleClick} />
}
このようなコンポーネントでレンダリングが走ると、handleClickは再び新規に作成されます。なので、レンダリング前と後ではpropsのonClickで参照する関数は別のものになります。
useCallbackは、このレンダリング時の関数の参照先をキャッシュしておきます。そうすることでpropsのonClickが参照している関数が変更されないため、無駄な子コンポーネントのレンダリングを抑制することができます。
useMemo,useCallbackはいつ使うのか
ここまでで読んでこう思いませんか?
じゃあ全部キャッシュすればいいじゃん
自分は思いました。なので、useMemo、useCallbackを使用する際のデメリットを踏まえてどのような場面で使用するべきなのかをまとめます。
デメリット
メモ化によるコスト
メモ化をする際にもそれなりのコストがかかります。
- 依存配列との比較
Reactがメモ化したcallback関数を実行する際には、単純に依存配列の値の(浅い)比較を順番に行います。
単純な処理であれば、依存配列の比較をしている処理の方がコストが大きくなります。 - キャッシュ管理 React内部では、「前回の結果の保持」と「新しい結果への入れ替え」を行います。これはメモリ操作なので軽いですがゼロではないです。
可読性の低下
全ての関数に、ラップと依存配列を記述しているとコードの可読性が低下します。また、依存配列のミスなどでバグの温床になることも懸念されます。
useMemoを使用するべき画面
重いロジックを含んでいる
重いロジックは単純に実行コストが大きいです。メモ化によるコストのほうが軽いであろう場合にはメモ化するべきでしょう。
memo関数を使用する場合
memo関数とはコンポーネントをメモ化するもので、useMemo、useCallbackと違いレンダリング自体を抑制することができます。
useMemo,useCallback単体ではレンダリング自体を抑制することはできません。そこでmemo関数と併用して、useMemoでpropsの参照を安定させることで、子コンポーネントの余計なレンダリングを抑制することができます。
useEffectの依存配列
依存配列の値の参照を安定させることで副作用による発火を抑制することができます。
useCallbackを使用するべき画面
memo関数を使用する場合
これはuseMemoの際のケースと同じです。propsの参照を安定させることで余計なレンダリングを抑制することができます。
イベントの登録
次のコードを見てください。
const [count, setCount] = useState(0)
const handler = () => console.log(count)
useEffect(() => {
window.addEventListener("resize", handler)
return () => window.removeEventListener("resize", handler)
}, [handler])
resizeイベントのハンドラを動的に登録する処理ですが、関数の参照に関して理解ができていればどんな問題が起きるかわかるでしょう。
レンダリングごとにhandlerの参照が変わってしまうため、レンダリングのたびにuseEffectが発火してイベントの着脱が毎回起きてしまいます。
そこでuseCallbackでhandlerの参照をキャッシュします。
const [count, setCount] = useState(0)
const handler = useCallback(() => {
console.log(count)
},[count])
useEffect(() => {
window.addEventListener("resize", handler)
return () => window.removeEventListener("resize", handler)
}, [handler])
handlerの参照をキャッシュしたことによってhandlerはcountの値が更新された時に再作成され、適切なタイミングでuseEffectを発火させることができるようになりました。
useEffectのロジック管理
次の処理を見てください。
useEffect(() => {
const utilFunc = (a, b) => {
// 汎用的な処理
}
},[a, b])
このような場合、utilFuncを他の箇所でも使用したい場合に別の場所でまた新規作成する必要があります。そこで以下のように関数を切り出しましょう。
const utilFunc = (a, b) => {
// 汎用的な処理
}
useEffect(() => {
utilFunc()
},[a, b]) // utilFuncも依存配列に入れるべきでは?
utilFuncを切り出すことができたのですが、次はutilFuncはレンダリングごとに参照が変わってしまうので依存配列に追加する必要が出てきます。
utilFuncはa,bの値に依存しているので依存配列にutilFuncを追加すると、a,bの更新とutilFuncの更新で二重にレンダリングが起きてしまいます。
もっと最悪なケースとして、以下の処理を見てください。
const utilFunc = (a, b) => {
const result = a + b
setResult(result)
}
useEffect(() => {
utilFunc()
},[a, b, utilFunc])
このような処理の場合、次のようなことが起きます。
- utilFuncが新しくなる
- effect発火
- setResultでstate更新
- 再レンダリング
- 新しいutilFunc生成
- effect再発火...
このように無限ループが発生してしまいます。useEffectの依存配列の参照が安定していないことが原因で複数の問題が起きてしまいます。
なのでuseCallbackで関数をメモ化しましょう。
const utilFunc = useCallback((a, b) => {
const result = a + b
setResult(result)
},[a, b])
useEffect(() => {
utilFunc()
},[utilFunc])
これでutilFuncはa,bの更新のタイミングでのみ再生成され、utilFuncは再生成されたタイミングでのみ処理を実行することができるようになりました。
まとめ
useMemo、useCallbackは値、関数をキャッシュし余計なレンダリングを抑制することができるものでした。 ただ、キャッシュするにあたりそれなりのコストもあるのでした。
- メモ化によるコスト
- 可読性の低下
メモ化は読んで字の如く「メモしておく」ことなので0コストで実装できるものではありません。ですが、適切に使用すれbばパフォーマンス改善に大きく貢献してくれます。
むやみやたらに使うのではなく、意味や期待値をちゃんと理解することでAIコーディングをより有意義にしましょう。使用するべき場面の例を下にまとめました。
useMemo
- 重いロジックを含んでいる
- memo関数を使用する場合
- useEffectの依存配列
useCallback
- memo関数を使用する場合
- イベントの登録
- useEffectのロジック管理
最後に
今回は、AIコーディングで頻出するuseMemo,useCallbackについて改めて学習してみました。正直今まではほとんど理解しないまま使用していたと反省しました。昨今のフロントエンド開発は、できることが多い分UIが複雑になりがちです。フロントのパフォーマンスもしっかりと考慮しないと最悪なUXになってしまうので今回の学習をもとに適切に使用していきたいと思います。
最後までご愛読ありがとうございました。

