Reactのパフォーマンスを最適化する方法を調べました【React.memo/React.useCallback/React.useMemo】

JavaScript

Reactはコンポーネントを組み合わせて作っていきます。
日頃あまり意識することはありませんが、一部に負荷の高いコンポーネントがあると、レンダリングの最適化を考える必要があります。
Reactのレンダリングの仕組みと、パフォーマンスの最適化について調べたのでメモしておきます。

コンポーネントが再レンダリングされる条件

まず、コンポーネントが再レンダリングされる条件は次の通りです。

  • Stateが更新された時
  • 変更されたPropsが渡された時
  • 親コンポーネントがレンダリングされた時

例として、次のようなサンプルを作成しました。
コンポーネントParentの子コンポーネントとして、Child1Child2があります。

Child1には、プルダウンメニュー、Child2にはラジオボタンがあります。
プルダウンで選択された値は、Stateで管理し、プルダウンが変更されるたびに更新されます。

コードをものすごく単純化したのがこちらです。

親コンポーネントがState更新で再レンダリングされた時、子コンポーネントも再レンダリングされる

親コンポーネントであるParentコンポーネントにStateがあるため、Child1コンポーネントのプルダウンが更新されると、Stateが更新→結果的に親コンポーネントが再レンダリングされます。

Parentコンポーネントは先程の再レンダリングされる条件

  • Stateが更新された時

に一致し、子コンポーネトChild1Child2

  • 親コンポーネントがレンダリングされた時

に条件が一致します。

親コンポーネントが再レンダリングされると、子コンポーネントも再レンダリングされます。
そのため、プルダウンを変更すると、結果的に全てのコンポーネントが再レンダリングされます。

どのような順番でレンダリングされるかは、以下のコンソールで確認できます。コンソールは、コンポーネントが更新されるタイミングで表示されるようにしています。

読み込み時とプルダウン変更時に、ParentChild1Child2全てのコンポーネントが再レンダリングされているのが確認できます。

変更されたPropsが渡された時、再レンダリングされる

再レンダリングされる条件をもう少し細かくみていきます。

Child1コンポーネントは、プルダウンを変更すると、更新されたStateの値(selectArea)がProps経由で渡されます。

<Child1
  onChangeArea={onChangeArea}
  setSelectArea={setSelectArea}
  selectArea={selectArea}
/>

先程の再レンダリングする条件

  • 変更されたPropsが渡された時

に一致するので再レンダリングされます。そもそもChild1コンポーネントは、Propsで受け取ったStateの値を、選択結果として表示しています。

上記を見て分かるように、変更された値をPropsで受け取ったタイミングで再レンダリングされるのは、必要な動きです。

不要な再レンダリングを制御したい

コンポーネントが再レンダリングするタイミングは分かりましたが、不要なレンダリングをスキップしたい時があると思います。その方法をまとめました。

コンポーネントをメモ化する【React.memo】

プルダウンを変更すると、ラジオボタンがあるChild2コンポーネントも再レンダリングされますが、本来は不要な挙動です。

不要なレンダリングをスキップしたい!そんな時に使えるのがReact.memoです。

React.memoは、コンポーネントをキャッシュ化、つまり一度使ったものを保存して不要なレンダリングを制御します。Reactでは「メモ化」と言うようです。

使用するには、importでReact.memoをインポートしておきます。

import { memo } from "react";

構文は次の通りです。

memo((コンポーネント));

早速、Child2コンポーネントをメモ化してみます。

変更前

const Child2 = () => {
  console.log("<Child2>がレンダリングされました");
  return (
    <div className="child2">
      <p className="conponemtName">Child2</p>
      <label htmlFor="radio-1">
        <input type="radio" name="gender" value="ラジオ1" id="radio-1" />
        <span className="mwform-radio-field-text">ラジオ1</span>
      </label>
      <label htmlFor="radio-2">
        <input type="radio" name="gender" value="ラジオ2" id="radio-2" />
        <span className="mwform-radio-field-text">ラジオ2</span>
      </label>
    </div>
  );
};

変更後

変更箇所に色をつけました。

const Child2 = memo(() => {
  console.log("<Child2>がレンダリングされました");
  return (
    <div className="child2">
      <p className="conponemtName">Child2</p>
      <label htmlFor="radio-1">
        <input type="radio" name="gender" value="ラジオ1" id="radio-1" />
        <span className="mwform-radio-field-text">ラジオ1</span>
      </label>
      <label htmlFor="radio-2">
        <input type="radio" name="gender" value="ラジオ2" id="radio-2" />
        <span className="mwform-radio-field-text">ラジオ2</span>
      </label>
    </div>
  );
});

memo()でコンポーネントを囲っているだけです。
これだけで、コンポーネントをメモ化できます。実際の動きは以下で確認できます!
プルダウンを変更しても、Child2コンポーネントが再レンダリングされていないことが確認できます。

メモ化しても再レンダリングされる場合がある

メモ化しても再レンダリングされる場合があります。それは、関数をPropsとして受け取った場合です。
例として、Parentコンポーネントにプルダウンの値をリセットする関数を作成します。

Parentコンポーネント

  // selectの選択をクリア
  const onClickClear = () => {
    setSelectArea("");
  };

上記は、単純にStateの値をリセットする関数です。

Child2コンポーネントに、この関数をPropsで渡します。

<Child2 onClickClear={onClickClear} />

「プルダウンの選択をリセットする」というボタンを用意し、クリックしたら発火するようにします。

コードはこちらです。

Child2コンポーネント

<button onClick={onClickClear}>プルダウンの選択をリセットする</button>

これをPropsとしてChild2コンポーネントに渡すと、メモ化したにも関わらず、再レンダリングされます。以下で動きを確認できます。

関数をメモ化する【useCallback】

Propsに渡す値に変更がない限り、コンポーネントは再レンダリングされないはずです。
今回の再レンダリングは、関数の再生成が関係しています。

実は、再レンダリングが起きると、同じ関数でも再生成され、別のものとして認識されるそうです。

今回は、Parentコンポーネントが再レンダリングされた時、再生成された関数をChild2コンポーネントにPropsで渡しているため、更新されたPropsとして認識されます。

再レンダリングされる条件、

  • 変更されたPropsが渡された時

に該当するため、Child2コンポーネントは再レンダリングされます。

対応としては、コンポーネントをメモ化したのと同じように、関数もメモ化してキャッシュできれば良さそうです。

そんな時に使えるのがReact.useCallbackです。

使用するには、importでReact.useCallbackをインポートします。

import { useCallback } from "react";

構文は次の通りです。

useCallback(関数, 依存配列)

先程の関数をuseCallbackでメモ化すると次のようになります。
※関係する箇所を色付けしています。

  // selectの選択をクリア
  const onClickClear = useCallback(() => {
    setSelectArea("");
  }, []);

依存する配列には変数を複数設定でき、設定した変数が変更されたタイミングで関数が再生成されます。今回、依存する変数はないので配列を空にします。
これで、最初に生成された関数がメモ化され、以降、使いまわされるようになります。

useCallbackを使うときの注意点

React.useCallbackは、メモ化したコンポーネントに渡して利用することで、関数の再生成をスキップできます。Rect.memoとセットで使います。

React.memoとReact.useCallbackを使ってパフォーマンスを最適化したサンプル

パフォーマンスを最適化したサンプルを残しておきます。
今までのサンプルと違い、コンポーネントを別ファイルに分割しています。プルダウンも汎用性の高いものに差し替えました。

変数をメモ化する【useMemo】

コンポーネント、関数以外に、変数をメモ化することもできます。
変数といっても文字列や単体の数字ではなく、特定の計算など、returnで返した戻り値をキャッシュして使いまわせるようにします。

戻り値が常に同じなら、再レンダリングの際に、計算などの処理をスキップできます。

次のような、負荷の高い計算結果をメモ化すると、パフォーマンス改善が期待できます。
※あくまで例です。うまいサンプルが思いつきませんでした、、。

// 負荷の高い(つもり)計算結果を変数に格納
const calc = () => {
    let result = 0;
    for(let i=0; i<10000; i++) {
      result += i;
    }
    return result;
  }

使用するには、importでReact.useMemoをインポートします。

import { useMemo} from "react";

構文は次の通りです。

useMemo(変数(処理の結果), 依存配列)

先程の変数をuseMemoでメモ化すると次のようになります。
※関係する箇所を色付けしています。

  const calc = useMemo(() => {
    let result = 0;
    for(let i=0; i<10000; i++) {
      result += i;
    }
    return result;
  }, [])

useCallbackと同様に、依存する配列には変数を複数設定できます。設定した変数が変更されたタイミングで、関数内の処理が実行されます。

useMemoは、コンポーネントの戻り値、つまり、レンダリング結果もメモ化できます。そのため、React.memoと同じような使い方をすることができます。

パッと思いつくのは、以下のようにコンポーネント内で、コンポーネントを設定したい時でしょうか。
今のところ使い所が分かりませんが、、。

import { useState, useMemo } from "react";
import "./styles.css";

const CountArea = (props) => {
  console.log("<CountArea>がレンダリングされました");

  const { count, setCount } = props;

  // useMemoで変数の戻り値をメモ化
  const Child1 = useMemo(() => {
    console.log("<Child1>がレンダリングされました");
    return <div>Child1</div>;
  },[]);

  return (
    <>
      <p>ContentArea</p>
      <p>Count:{count}</p>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>
        カウントアップ
      </button>
      {Child1}
    </>
  );
};

export default function App() {
  console.log("---------レンダリングが開始されました");

  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <CountArea count={count} setCount={setCount} />
    </div>
  );
}

まとめ

メモ化は、コンポーネントに負荷の大きな処理が発生する時に使用します。
今回作ったサンプルは、どれもメモ化する必要はないと思っています。あくまで動きを確認するために作りました。

パフォーマンスが悪いと感じた時にメモ化を思い出して使おうと思います!

コメント