useMemo と useCallbackをいつ使用するか
お菓子の自動販売機があります。
Candy Dispenser
* Available Candy
* grab snickers
* grab skittles
* grab twix
* grab milky way
こちらが実装になります。
function CandyDispenser() {
const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const [candies, setCandies] = React.useState(initialCandies)
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
return (
<div>
<h1>Candy Dispenser</h1>
<div>
<div>Available Candy</div>
{candies.length === 0 ? (
<button onClick={() => setCandies(initialCandies)}>refill</button>
) : (
<ul>
{candies.map(candy => (
<li key={candy}>
<button onClick={() => dispense(candy)}>grab</button> {candy}
</li>
))}
</ul>
)}
</div>
</div>
)
}
さて、ここで皆さんに質問をしたいのですが、上記のコードに手を加えるのでどちらの方がパフォーマンスが良くなるのか教えて欲しいです。
dispense
関数をReact.useCallback
でラッピングする方法。
const dispense = React.useCallback(candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])
もう一つはそのままの実装です。
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
さてこの場合、どちらがパフォーマンス的に優れているでしょうか?
original
正解!!
useCallback
不正解..
答えのネタバレにならないようにスペースを空けて...
更にスクロールして.. ちゃんと答えましたか??
useCallback
の方が悪いのか?
なぜインライン関数はパフォーマンス上問題がある
のでパフォーマンス向上の為にReact.useCallback
を使うべきということをよく聞きますが、なぜuseCallback
を使わない方が良いのでしょうか?
具体例から、そしてReactからも一歩引いて実行されるコード全てにコストがかかること
について考えてみてください。useCallback
の例を少しリファクタして(実際の変更はなく、コードを分離しただけです)、より明確に説明しましょう。
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])
そしてここでオリジナルを再掲します。
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
何か気付きましたか? 差分を見てみましょう
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
+ const dispenseCallback = React.useCallback(dispense, [])
useCallback
バージョンの方がより多くの作業を行なっている以外は、全く同じです。
関数を定義するのではなく、配列([]
)を定義してReact.usecallback
を呼び出し、それ自体がプロパティの設定/論理式の実行などを行なっています。
つまり、どちらの場合もJavaScriptはレンダリングの度に関数定義のためのメモリを確保しなければならず、useCallback
の実装によっては、より多くのメモリを確保するかもしれません(実際にはそうではありませんが、要点は変わりません)。これが、私がこちらのTwitter投票で伝えようとしたことです。
コンポーネントの2回目のレンダリング時に、オリジナルのdispense
関数がガベージコレクション(メモリ領域が開放)され、その後新しい関数が作成されることにも触れておきたいと思います。しかし、useCallback
を使用すると、オリジナルのdispense
関数はガベージコレクションされず、新しい関数が作成されるため、メモリの観点からも不利になります。
関連する事として、依存関係がある場合、Reactが以前の関数への参照を保持している可能性が高いです。なぜなら、メモ化は通常、以前の値と同じ依存関係を得た場合に返すための古い値のコピーを保持することを意味するからです。特に勘のいい人は、Reactも等価であるかを確認する為に依存関係の参照を保持しなければならないことに気付くでしょう。(ちなみに、クロージャのおかげでどのみち起こっていることですが、とにかく言及する価値のあることです。)
useMemo
は?
よく似たuseMemo
はuseCallback
と似ていますが、あらゆる値の型(関数だけでなく)にメモ化を適用できる点が異なります。これは値を返す関数を受け取り、その関数は値を取得する必要がある場合のみ呼び出されます。(通常、レンダリング中に依存関係の配列内の要素が変更される度に一度だけ実行されます。)
つまり、レンダリングの度にinitialCandies
の配列を初期化したくない場合は、このように変更すればよいのです。
- const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
+const initialCandies = React.useMemo(
+ () => ['snickers', 'skittles', 'twix', 'milky way'],
+ [],
+)
そして、その問題を回避したいのですがやれることはごくわずかで、コードをより複雑にするコストに見合うものではありません。実際、このような場合にもuseMemoを使うことは悪いことかもしれません。なぜなら、ここでも関数を呼び出して、そのコードでプロパティの割り当てなどを行なっているからです。
この特別なシナリオでの更に良い方法は下記の変更を行うことです。
+ const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
function CandyDispenser() {
- const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const [candies, setCandies] = React.useState(initialCandies)
しかし、時には上記のような贅沢は許されません。なぜなら、props
や関数本体の中で初期化された変数から渡される場合があるからです。
要はどっちでも良いのです。そのコードの最適化することのメリットは非常に小さく、プロダクトをより良くすることに時間を費やした方がずっと良いのです。
ポイントは?
要はこういうことです。
パフォーマンスの最適化はタダではありません。最適化には常にコストが掛かりますが、そのコストを相殺するようなメリットが常にあるわけではありません。
したがって、責任を持って最適化する必要があります。
useMemo
とuseCallback
を使えば良いの?
では、どのような場合にこの2つのフックがReactに組み込まれているのには、特別な理由があります。
- Referential equality(参照元が同じ)
- Computationally expensive calculations(重い処理)
Referential equality
あなたがJavaScript初心者の場合、その理由が分かるまでにそう時間は掛からないでしょう。
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
(() => {}) === (() => {}) // false
const z = {}
z === z // true
// NOTE: React actually uses Object.is, but it's very similar to ===
あまり深入りするつもりはないですが、React関数コンポーネント内でオブジェクトを定義した時、その同じオブジェクトが前回定義された時と参照的に等しくなることはない(たとえ、全てのプロパティが同じ値であっても)、とだけ言っておけば十分だろう。
ReactでReferential equalityが問題となる場合は2つあり、1つずつ見ていきましょう。
依存関係のリスト
例を見てみましょう。
function Foo({bar, baz}) {
const options = {bar, baz}
React.useEffect(() => {
buzz(options)
}, [options]) // we want this to re-run if bar or baz change
return <div>foobar</div>
}
function Blub() {
return <Foo bar="bar value" baz={3} />
}
これが問題になるのは、useEffect
がレンダリングの度にoptions
の参照一致を確認しますが、JavaScriptのおかげでoptions
は毎回新しくなる事です。そのため、Reactがレンダリング中にoptions
が変更されたかどうかをテストする時は、常にtrue
と評価されます。つまり、useEffect
コールバックはbar
とbaz
が変化した時ではなくレンダリング毎に呼び出されるようになります。
これを解決するために、できることは2つあります。
// option 1
function Foo({bar, baz}) {
React.useEffect(() => {
const options = {bar, baz}
buzz(options)
}, [bar, baz]) // we want this to re-run if bar or baz change
return <div>foobar</div>
}
これは素晴らしいオプションで、もしこれが実際のシチュエーションなら私はこの方法で修正します。
しかし、これが実用的な解決策でない場合が1つあります。bar
,baz
が(非プリミティブな)オブジェクト/配列/関数/その他である場合です。
function Blub() {
const bar = () => {}
const baz = [1, 2, 3]
return <Foo bar={bar} baz={baz} />
}
これこそがuseCallback
とuseMemo
が存在する理由なのです。では、どのような修正を行うかというと次のようになります。(今は全部まとめて)
function Foo({bar, baz}) {
React.useEffect(() => {
const options = {bar, baz}
buzz(options)
}, [bar, baz])
return <div>foobar</div>
}
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
return <Foo bar={bar} baz={baz} />
}
React.memo
(and friends)
こちらをご覧ください。
function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
}
function DualCounter() {
const [count1, setCount1] = React.useState(0)
const increment1 = () => setCount1(c => c + 1)
const [count2, setCount2] = React.useState(0)
const increment2 = () => setCount2(c => c + 1)
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
)
}
これらのボタンのいずれかをクリックする度に、DualCounter
のステート変化するので再レンダリングし、その結果、両方のCountButton
が再レンダリングすることになります。しかし、実際に再レンダリングが必要なのは、クリックされたボタンのみのはずですよね? つまり、最初のボタンをクリックすると、2番目のボタンが再レンダリングされますが、何も変化はありません。これを私達は不要な再レンダリング
と呼んでいます。
ほとんどの場合、不要な再レンダリングを最適化する必要はないでしょう。
Reactは非常に高速で、このような最適化よりも、時間を掛けてやるべき事がたくさんあると思います。実際、これからお見せするもので最適化する必要性は非常に稀で、私がPayPalのプロダクトで働いた3年間、そして更に長い期間において、文字通り一度も必要なことはありませんでした。
しかし、レンダリングにかなりの時間がかかる場合があります(高度な相互作用を持つグラフ/チャート/アニメーションなどを考えてみてください)。Reactの実用的な性質のおかげで、逃げ道があります。
const CountButton = React.memo(function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
})
これでReactはCountButton
のpropsが変更された時のみ再レンダリングするようになりました。しかし、まだ終わりではありません。参照の等価性というのを覚えていますか? DualCounter
コンポネートでは、increment1
,increment2
をコンポネート内で定義しています。これはDualCouter
が再レンダリングする度に、これらの関数が新しくなり、それゆえにReactはCountButton
の両方を再レンダリングすることを意味します。
つまり、これはuseCallback
とuseMemo
が役立つもう1つの状況です。
const CountButton = React.memo(function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
})
function DualCounter() {
const [count1, setCount1] = React.useState(0)
- const increment1 = () => setCount1(c => c + 1)
+ const increment1 = React.useCallback(() => setCount1(c => c + 1), [])
const [count2, setCount2] = React.useState(0)
- const increment1 = () => setCount2(c => c + 1)
+ const increment2 = React.useCallback(() => setCount2(c => c + 1), [])
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
)
}
これでCountButton
の「不要な再レンダリング」を回避することができます。
計測せずにReact.memo
(もしくはその友人であるPureComponent
とshouldComponentUpdate
)を使用しないことを強くお勧めすることを再度述べたいと思います。なぜなら、これらの最適化にはコストが掛かるため、そのコストに関連する利益を確認し、実際にあなたのケースに役立つかどうか(有害でないかどうか)を判断する必要があるからです。
上記で述べたようにメリットが全く得られない可能性があります
。
Computationally expensive calculations
useMemo
がReactの組み込みフックであるもう1つの理由がこれです(こちらはuseCallback
に当てはまらない事に注意してください)。useMemo
の利点は、次のような値を取れる事です。
const a = {b: props.b}
そして遅延評価を行う
const a = React.useMemo(() => ({b: props.b}), [props.b])
これは上記のケースではあまり役に立ちませんが、計算コストの掛かる値を同期的に計算する関数があるとします(実際にこのような素数計算が必要なアプリがどれだけあるかという事ですが、これは一例です。)
function RenderPrimes({iterations, multiplier}) {
const primes = calculatePrimes(iterations, multiplier)
return <div>Primes! {primes}</div>
}
このコードは適切なiterations
もしくはmultiplier
が渡されると非常に処理が遅くなる可能性があり、
具体的にどうすれば良いのかはあまり分かりません。ユーザーのハードウェアを自動的に速くすることはできません。しかし、同じ値を2回続けて計算する必要がないようにすることはできます。それがuseMemo
の役割です。
function RenderPrimes({iterations, multiplier}) {
const primes = React.useMemo(
() => calculatePrimes(iterations, multiplier),
[iterations, multiplier],
)
return <div>Primes! {primes}</div>
}
これがうまくいく理由は、レンダリング毎に素数を計算する関数を定義しても(これは非常に高速です)、Reactは値が必要な時にだけその関数を呼び出すからです。その上で、Reactは入力された前の値も保存し、同じ前の入力があれば前の値を返すようにする。これがメモ化です。
結論
最後に、全ての抽象化(そしてパフォーマンス最適化)にはコストが掛かることを申し上げたいと思います。
本当に必要になるまで抽象化/最適化を行わないAHA Programming principleを適用することで、コストがかかってもメリットが得られないという事態を避けることができます。
具体的には、useCallback
とuseMemo
の代償として、同僚にとってコードを複雑にしてしまうこと、依存関係の配列でミスをすること、組み込みフックを呼び出して依存関係やメモした値がガベージコレクションされないようにしてパフォーマンスを悪化させる可能性があることなどです。必要な性能が得られるのであれば、それらは全て十分なコストですが、まずは測定してみるのが一番です
。
関連記事:
- React FAQ: "Are Hooks slow because of creating functions in render?"
- Ryan Florence: React, Inline Functions, and Performance
追記
フックの移行に伴い、クラスのメソッドとして定義していたものを関数コンポーネントの中で定義しなければならなくなることを心配される方がいらっしゃいますが、私達は当初からコンポネートのレンダーフェーズででメソッドを定義しているという事実を考慮して頂きたいと思います...。例えば:
class FavoriteNumbers extends React.Component {
render() {
return (
<ul>
{this.props.favoriteNumbers.map(number => (
// TADA! This is a function defined in the render method!
// Hooks did not introduce this concept.
// We've been doing this all along.
<li key={number}>{number}</li>
))}
</ul>
)
}
}
Discussion