React.memo()を賢く使う
この記事ではReact.memo()
を適切に利用してパフォーマンスを向上させる方法について説明します。
React.memo()
コンポーネントがReact.memo()
でラップされると、Reactはコンポーネントをレンダリングし、結果をメモ化します。次のレンダリングの前に、新しいprops
が同じである場合、Reactはメモ化された結果を再利用して次のレンダリングをスキップします。
例: React.memo()
でラップされた関数コンポーネントのMovie
をみてみます。
export function Movie({ title, releaseDate }) {
return (
<div>
<div>Movie title: {title}</div>
<div>Release date: {releaseDate}</div>
</div>
);
}
export const MemoizedMovie = React.memo(Movie);
React.memo(Movie)
は、新しいメモ化されたコンポーネントであるMemoizedMovie
を返します。
MemoizedMovie
は、レンダリングされたコンテンツをメモ化します。titleまたはreleaseDate props
がレンダリング前後で同じであるとき、メモ化されたコンテンツを再利用します。
メモ化されたコンテンツを再利用することで、パフォーマンスが向上します。Reactは、コンポーネントのレンダリングをスキップします。
ClassコンポーネントではPureComponent
を使う事で同じ機能を実装できます。
React.memo()をいつ使うか
React.memo()でコンポーネントをラップすべきケースは、関数コンポーネントが頻繁にレンダリングされ、同じprops
でレンダリングされることが予想される場合です。
よくあるケースは、親コンポーネントによって強制的にレンダリングされている場合です。
Movie
上で定義したコンポーネントを再利用し、新しい親コンポーネントMovieViewsRealtime
で、リアルタイムの更新とともに、映画の視聴回数を表示する場合を考えます。
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<Movie title={title} releaseDate={releaseDate} />
視聴回数: {views}
</div>
);
}
これは定期的にMovieViewsRealtime
コンポーネントのviews
を更新します。
//初期レンダ-
<MovieViewsRealtime
views={0}
title="honyohonyo"
releaseDate="June 23, 2020"
/>
//1秒後
<MovieViewsRealtime
views={10}
title="honyohonyo"
releaseDate="June 23, 2020"
/>
//2秒後
<MovieViewsRealtime
views={25}
title="honyohonyo"
releaseDate="June 23, 2020"
/>
この場合、views
propsが新しい番号で更新される度に、MovieViewsRealtime
がレンダリングされます。これによりMovie
コンポーネントのtitle
とreleaseDate
が同じであっても、レンダリングされてしまいます。
これは、Movie
コンポーネントにメモ化を適用するのに適したケースです。
無駄な再レンダリングを防ぐために、メモ化されたMovieViewsRealtime
を利用します。
return (
<div>
<MemoizedMovie title={title} releaseDate={releaseDate} />
視聴回数: {views}
</div>
)
}
title
とreleaseDate
のpropsが同じである限り、MemoizedMovie
のレンダリングをスキップします。これによって、MovieViewsRealtime
コンポーネントのパフォーマンスが向上します。
コンポーネントが同じpropsでレンダリングされる頻度が高いほど、出力が重くなり計算コストが高くなるので、 コンポーネントをReact.memo()でラップする必要性が高くなります。
React.memo()とコールバック関数
親コンポーネントが子のコールバックを定義するたびに、新しい関数インスタンスが作成されます。これがメモ化をどのように破壊するか、そしてそれを修正する方法を確認します。
次のLogout
コンポーネントは、コールバックプロパティonLogout
を持ちます。
function Logout({ username, onLogout }) {
return (
<div onClick={onLogout}>
Logout {username}
</div>
);
}
const MemoizedLogout = React.memo(Logout);
function MyApp({ store, cookies }) {
return (
<div className="main">
<header>
<MemoizedLogout
username={store.username}
onLogout={() => cookies.clear('session')} />
</header>
{store.content}
</div>
);
}
このとき、MemoizedLogout
で同じusername
が指定されている場合でも、onLogout
コールバックの新しいインスタンスを取得するため、毎回レンダリングされます。
これではメモ化が破壊してます😱
これを修正するには、onLogout
propが同じコールバックインスタンスを受け取る必要があります。useCallback()
を利用して、レンダリング間でコールバックインスタンスを保持します。
function MyApp({ store, cookies }) {
const onLogout = useCallback( () => cookies.clear('session'), [cookies] ); return (
<div className="main">
<header>
<MemoizedLogout
username={store.username}
onLogout={onLogout} />
</header>
{store.content}
</div>
);
}
useCallback(() => cookies.clear('session'), [cookies])
によって、cookies
が同じである限り、常に同じ関数インスタンスを返します。これでMemoizedLogout
のメモ化が修正されました。
まとめ
- React.memo()を正しく利用すると、次のpropsが前のpropsと等しいときに無駄な再レンダリングを防ぐ。
- propsをコールバックとして使用するコンポーネントをメモ化するときは、注意が必要。レンダリング間で同じコールバック関数インスタンスを利用するようにする。
以上😎