React.memoの特徴と使い方
背景
reactのパフォーマンスチューニングについて調べた時、React.memoの特徴と使い方を深く学習することができたので、備忘録としてまとめておきます。
React.memoについて学習している方の参考になれば幸いです。
React.memoとは?
React.memoは、コンポーネントの props が変更された場合のみ再レンダリングを行うためのものです。
props が変更されない限り、前回のレンダリング結果を再利用します。つまり、React.memoを使用することで コンポーネントに渡る props が変更されていない場合、そのコンポーネントの再レンダリングをスキップすることができます。
ドキュメント
React.memoを使用するときは、下記のような書き方をします。
import { memo } from 'react’
const Count = memo(( props ) ⇒ { });
props以外の値が渡ってきても再レンダリングされないが、propsの値が渡ってきてその値が変更されたら再レンダリングされます。
また、親componentのstateが更新されても、React.memoを使用した子componentに渡されるpropsが変更・更新されない限り、再レンダリングされません。
具体的な使用例は以下の通りです。
import React from 'react';
function MyComponent({ value }) {
return <div>{value}</div>;
}
// propsが変更されない限り再レンダリングをスキップする
const MemoizedComponent = React.memo(MyComponent);
function ParentComponent() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<MemoizedComponent value={count} />
</div>
);
}
上記の例では、MyComponent
コンポーネントを React.memo
でラップし、React.memo
によって MemoizedComponent
は value
の値が変更されない限り再レンダリングをスキップしています。
これにより、ParentComponent
の再レンダリング時にも props のvalue に変化がなければ、MemoizedComponent
が再計算されずに前回の結果を再利用することができます。
ただし、React.memo
を使用する際にはいくつかの注意点があります。
React.memo
は shallow compare (参照先かどうかの比較)を行いますが、複雑なオブジェクトや配列の場合は変更を検出できない可能性があります。この場合は、カスタム比較関数を指定することで 対応することができます。
React.memo
は再レンダリングの最適化に貢献しますが、必ずしもすべてのコンポーネントで使用する必要はありません。単純なコンポーネントや再レンダリングのコストが低い場合は、React.memo
を適用しなくても十分なパフォーマンスを実現できる場合もあります。
React.memoのpropsの比較について
React.memoでは、props が変更された場合のみ再レンダリングを行います。
つまり、React.memoでは、再レンダリングする必要があるかどうかを判断するために、前回のpropsの値と現在のpropsの値の比較を行います。
Reactでは等価性の比較を Object.is を用いて行なっており、React.memo の props の比較やuseEffect や useCallback, useMemo の依存配列の比較などにも Object.is が使用されています。
ただし、オブジェクトや配列、関数などのオブジェクト型(プリミティブ型ではないもの)を比較する場合、値そのものではなく参照先データを比較します。
参考
つまり、同じキーと値を持つオブジェクトや同じ値を持つ配列、同じコードで書かれた関数でも、再レンダリング前後では異なるものとして認識されてしまいます。
したがって、React.memoを使用したコンポーネントにオブジェクト型のpropsを渡すと、propsの変更を検知し、再レンダリングが発生します。
参考記事
カスタム比較関数
メモ化されたコンポーネントの props の変更を最小限にすることが困難な場合に、カスタム比較関数(前回のpropsと新しいpropsを受け取り、それらを比較して同じかどうか判断する関数)を使用することができます。
React.memoの第二引数にこの関数を設定することでこの関数を使用することができ、古い props と新しい props を比較し、同じ出力結果をもたらす場合には再レンダリングを抑制することができます。
下記がカスタム比較関数の使用例となります。
const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);
function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}
上記のコードは、Chart
というコンポーネントを React.memo
でメモ化しています。メモ化されたコンポーネントは、dataPoints
という props が変更された場合にのみ再レンダリングされます。
arePropsEqual
というカスタム比較関数が定義されており、これが Chart
コンポーネントの再レンダリングの条件を制御しています。この関数は古い props と新しい props を受け取り、それらが同じ出力結果をもたらすかどうかを判定しています。
具体的には、arePropsEqual
関数では、dataPoints
の配列の長さが同じであり、各要素の x
と y
の値が全て一致しているかどうかをチェックしています。配列の要素ごとに比較を行い、すべての要素が一致している場合にのみ再レンダリングを抑制します。
このように、arePropsEqual
関数を使用することで、Chart
コンポーネントは dataPoints
の中身が変更されない限り再レンダリングされません。つまり、dataPoints
の要素が同じであれば、コンポーネントの再レンダリングが抑制されます。これにより、不要な再レンダリングを避け、パフォーマンスを向上させることができます。
参考
関数を props として渡しているとき
関数をpropsとして渡すときは、再レンダリングされるたびに props に変更があったとみなされ、関数を再生成するので、React.memoでは再レンダリングを防ぐことはできません。
この場合、useCallbackを使用することで不要な再レンダリングを防ぐことができます。
参考
まとめ
以上が公式ドキュメントをもとに自分なりにまとめたReact.memoの特徴と使い方になります。
今後は、React.memoをうまく活用し、reactアプリケーションのパフォーマンスチューニングを行なっていきたいです。
Discussion