😎

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" 
/>

この場合、viewspropsが新しい番号で更新される度に、MovieViewsRealtimeがレンダリングされます。これによりMovieコンポーネントのtitlereleaseDateが同じであっても、レンダリングされてしまいます。

これは、Movieコンポーネントにメモ化を適用するのに適したケースです。

無駄な再レンダリングを防ぐために、メモ化されたMovieViewsRealtimeを利用します。

  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      視聴回数: {views}
    </div>
  )
}

titlereleaseDateの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コールバックの新しいインスタンスを取得するため、毎回レンダリングされます。 これではメモ化が破壊してます😱

これを修正するには、onLogoutpropが同じコールバックインスタンスを受け取る必要があります。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をコールバックとして使用するコンポーネントをメモ化するときは、注意が必要。レンダリング間で同じコールバック関数インスタンスを利用するようにする。

以上😎

大学生です。エンジニアでもあります。好きなもの作ってます。