React 最適化
useMemo、useCallback、React.memo のコストについて気になったので調べる
スクラップを作成した時点での自分の結論
・メモ化したコンポーネントに関数 or オブジェクトを渡すときは useCallback、useMemo をつかえ
・カスタムフックで関数、オブジェクトを返すときは useCallback、useMemo をつかえ
・それ以外は基本的に必要ない
余計な div は useMemo よりもコストが高い
uhyo さんがより詳細にまとめてくれている
余計なuseMemoが10,000個ある場合、1.7ミリ秒程度のオーバーヘッドがあるようです。これはMac上での結果なので、スマートフォン対象の超大規模アプリだと10ミリ秒単位の差が出るかもしれません。1個あたりだと0.00017ミリ秒くらいのオーバーヘッドと概算できますね。
さらに、驚くべきことに、余計なdivが10,000個ある場合は3.0ミリ秒程度のオーバーヘッドが観測されています。useMemoの約1.76倍です。
自分のベンチマーク結果 (Mac Mini / M2 Pro / 16G)
div は useMemo の2倍以上オーバーヘッドがある
ただ、数値を見る限りオーバーヘッドはないに等しいのではないか?
今いる関数の外を見に行くな。そのuseMemoが今または将来に役に立つ可能性が1%でもあるなら使え
将来コンポーネントをメモ化するとなった場合に、親で useMemo や useCallback を追加する必要があるくらいであれば、最初から使っておいた方が良い。メモ化のオーバーヘッドは恐るるに足らない。
メモによる最適化は、コンポーネントが同じプロップで頻繁に再レンダリングし、その再レンダリングロジックが高価である場合にのみ価値があります。コンポーネントが再レンダリングするときに知覚できるようなラグがなければ、メモは不要です。
useCallbackを使った関数のキャッシュは、いくつかの場合にのみ価値がある
- メモでラップされたコンポーネントにpropとして渡す。値が変わっていなければ、再レンダリングをスキップしたい。メモ化は、依存関係が変更された場合にのみコンポーネントを再レンダリングさせます。
- 渡された関数は、後に何らかのHookの依存関係として使用されます。例えば、useCallbackでラップされた別の関数がこの関数に依存していたり、useEffectからこの関数に依存していたりします。
それ以外のケースでは、関数をuseCallbackでラップするメリットはない。だから、個々のケースについて考えず、できるだけメモすることを選択するチームもある。デメリットは、コードが読みにくくなることだ。
useCallbackは関数の作成を妨げないことに注意してください。しかし、Reactはそれを無視し、何も変更がなければキャッシュされた関数を返します。
=> つまり関数再生成のコスト削減にはつながらない?
メモ化されたコールバックからの状態更新
これは
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
こう書くべき
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...
カスタムフックの最適化
カスタムHookを書く場合は、そのHookが返す関数をすべてuseCallbackにラップすることをお勧めします
uhyo さん意見と同じだ
そもそもレンダリングという言葉が曖昧。
React の文脈では描画とレンダリングを区別する必要がある。
レンダリングとはコンポーネント (関数) を実行することである。
= 仮想DOMを構築し、前回との差分を比較すること
memo やuseMemo のような最適化を適用する前に、変更する部分と変更しない部分を分けることができるかどうかを調べることは意味があるかもしれない。