🐥

【React】 レンダリングの最適化についてまとめる

2023/09/24に公開
2

はじめに

Reactのレンダリングの最適化の手法(React.memo,useMemo,useCallback)について、
学んだことを簡単にまとめていきたいと思います。

再レンダリングのタイミングについて

Reactで再レンダリングが行われるタイミングは以下3点です。

  1. 親コンポーネントが再レンダリングされた場合
  2. stateが更新された場合
  3. propsが更新された場合

1. 親コンポーネントが再レンダリングされた場合

以下のように、親コンポーネント(Parent)配下に子コンポーネント(Child1〜Child3)が
存在する場合、親コンポーネント(Parent)が更新されるされると、
赤枠で囲んだ子コンポーネント(Child1〜Child3)が再レンダリングされます。

コンポーネント図Parent更新前

Parent更新後

2. stateが更新された場合

useState等を用いてstate(状態)を持った変数が更新されると、
そのstate(状態)をもったコンポーネントが再レンダリングされます。

3. propsが更新された場合

親コンポーネント(Parent)から子コンポーネント(Child)に渡すpropsの中身が更新されると、
子コンポーネント(Child)が再レンダリングされます。

※追記
propsが更新されるのは、親コンポーネントが再レンダリングされた場合のみなので、「1. 親コンポーネントが再レンダリングされた場合」と実質同義となる。

レンダリングの最適化手法について

レンダリングを最適化する手法には以下3つの方法があります。

  1. React.memo
  2. useMemo
  3. useCallback

これら3つの手法を用いてメモ化することで、余計な再レンダリングを防ぐことができます。
それそれの機能について順に解説していきます。

1. React.memo

React.memoではコンポーネントのpropsが変更されない限り、再レンダリングは行われません。
つまり、親コンポーネントが再レンダリングされても、渡されるpropsが変更されなければ
再レンダリングは行われないということです。

https://react.dev/reference/react/memo

React.memoの使用方法は以下の通りです。

import React from "react";

const Component = React.memo(( props ) => { /* 何か記述 */});

このように記述することで、親コンポーネントが再レンダリングされても、propsの中身が更新されない限り、再レンダリングは行われません。

2. useMemo

useMemoはReact Hooksの1つであり、再レンダリングの際の計算結果をキャッシュするために使用します。
計算結果をキャッシュすることで、stateが変わらなくなるため、再レンダリングを防ぐことができます。

https://react.dev/reference/react/useMemo

useMemoの使用方法は以下の通りです。

import React, { useState, useMemo } from "react";

const Component = () => {
  const [ temp1, setTemp1 ] = useState(0);
  const [ temp2, setTemp2 ] = useState(0);
  
  const data = useMemo(() => { /* temp1とtemp2を用いた計算処理 */}, [temp1, temp2]);
  
  return (
    <>
    // ...省略
    </>
  );
};

useMemoの第一引数には変数(値)を返す関数、第二引数には配列を指定し、
配列の中身には更新を行う際のトリガとなる変数を入れます。

3. useCallback

useCallbackはReact Hooksの1つであり、再レンダリングの際の関数定義をキャッシュするために使用します。
関数定義をキャッシュすることで、propsの更新が行なわれなくなるため、再レンダリングを防ぐことができます。

https://react.dev/reference/react/useCallback

useCallbackの使用方法は以下の通りです。

import React, { useCallback } from "react";

const Child = React.memo(({ handleClick }) => {
  return (
    <>
      <p>子コンポーネント</p>
      <button onClick={handleClick}>Child</button>
    </>
  );
});

const Parent = () => {
  const useCallback(handleClick = () => {
    // ボタンクリック時の処理
  }, []);

  return (
    <>
      <p>親コンポーネント</p>
      <Child handleClick={handleClick} />
    </>
  );
};

useCallbackの第一引数には関数、第二引数には配列を指定し、
配列の中身には関数を再生成する際のトリガとなる変数を入れます。
(空にすると最初の一度のみ関数が生成されます。)

最後に

簡単ながら、Reactのレンダリングの最適化についてまとめてみました。
あまりレンダリングに関しては意識できていない部分があったので、
今後実装する際はこれらのことを意識して実装していきたいと思いました。

参考文献

https://react.dev/
https://zenn.dev/b1essk/articles/react-re-rendering

Discussion

Honey32Honey32

コンポーネントの再レンダリングが発生するタイミングは以下の2つだけです。

  1. このコンポーネントのステートが更新された
  2. 親コンポーネントが再レンダリングされた (Props が変わらなくても)

なぜなら、 Props が変わる原因は親コンポーネントの再レンダリングだけ、つまり「 親が再レンダリングされるProps が更新される」という関係だからです。

コンポーネントを React.memo で囲うと、「親のコンポーネントが再レンダリングされても、Props が変わっていなければ、再レンダリングをスキップする」 ようになります。

なので、再レンダリング発生の条件は以下のように変わります。

  1. このコンポーネントのステートが更新された
  2. Props が更新された

なので、React.memo で囲われていないコンポーネントに渡すコンポーネントに useCallback でメモ化された関数を渡しても、「Props が変わらなくても、親コンポーネントが再レンダリングされた」条件に引っかかってしまうので再レンダリングは発生します。

また、React.memo には useMemouseCallback を一箇所でつけ忘れただけで破綻する、という欠点があるので、僕は以下のセクションにあるように、

  • ステートの配置を見直す
  • children Props を使う

のように、コンポーネントの構造を見直すことを第一に考えるほうが良いと思います。

https://react.dev/reference/react/memo#should-you-add-memo-everywhere

https://qiita.com/honey32/items/2e6206c7dc1974b9bf9a

snow_swallowsnow_swallow

ご指摘ありがとうございます!
確かに仰る通り、Propsが変わる原因は親コンポーネントが再レンダリングされた場合のみなので、「3. propsが更新された場合」は条件になくても問題ありませんね...
失礼いたしました。

React.memo には useMemo や useCallback を一箇所でつけ忘れただけで破綻する、> という欠点があるので、僕は以下のセクションにあるように、

  • ステートの配置を見直す
  • children Props を使う
    のように、コンポーネントの構造を見直すことを第一に考えるほうが良いと思います。

勉強になります!
仰る通りコンポーネントの構造を見直す方を優先したいと思います。

指摘いただいた内容は、追記・修正したいと思います!