Reactパフォーマンス最適化まとめ

## はじめに

自分は2021年に新卒でWeb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。

実務ではReact×TypeScriptを利用したフロント周りの開発をメインで行なっています。

今回は、現場で経験したReactアプリのパフォーマンス最適化についてまとめていきます。

## この記事の対象者

- Reactの初心者から中級者
- Reactのパフォーマンス最適化について学びたい人

## この記事の目標

- Reactのレンダリングの仕組みを理解する
- Reactのパフォーマンス最適化の方法を知る
- ``React.memo``, ``useCallback``, ``useMemo``について理解する

## おことわり

- ``React.memo``, ``useCallback``, ``useMemo``を使うコストについての詳しい解説
- パフォーマンスの数値的な計測は行いません

上記の2点に関しては参考記事を該当箇所で貼ります。

## Reactのレンダリングについて

Reactのパフォーマンス最適化をするにあたってReactのレンダリングの仕組みについて解説します。

まずレンダリングとは、[React Docs BETA](https://beta.reactjs.org/learn/render-and-commit)では下記のように説明されています。(DeepL翻訳を使用済み)

> 「レンダリング」とは、React がコンポーネントを呼び出すことです。
> - 最初のレンダリングでは、React はルート コンポーネントを呼び出します。
> - それ以降のレンダリングでは、React はレンダリングのトリガーとなった状態更新の関数コンポーネントを呼び出します。

一言で、Reactが関数コンポーネントを呼び出すことを**レンダリング**と呼んでいます。

レンダリングの仕組みは[React Docs BETA](https://beta.reactjs.org/learn/render-and-commit)では下記のように記載されています。

> 1. レンダリングをトリガーする（客の注文を厨房に届ける）
> 2. コンポーネントのレンダリング（厨房で注文を準備する）
> 3. DOMにコミットする（テーブルに注文を置く）

上記のレンダリングの仕組みを少し噛み砕いて説明していきます。

#### 1. レンダリングをトリガーする

レンダリングのトリガーとなるイベントは下記の2つです

- 初回レンダリング
- 画面更新時の再レンダリング

再レンダリングは、``state``の更新関数``setState``を利用して状態を更新し、コンポーネントの状態を更新することで、次回レンダリングをスケジューリングします。(差分を取得し更新)

#### 2. Reactがコンポーネントをレンダリングする

レンダリングが開始されると、Reactコンポーネントを呼び出し画面に表示する内容を決定します。この時点ではDOMへの反映処理は行っていません。

- 初回レンダリング: Reactはルートコンポーネントを呼び出す
- それ以降のレンダリング: 1でトリガーとなった状態更新の関数コンポーネントを呼び出す

更新されたコンポーネントが子コンポーネントを持っている場合、Reactはその子コンポーネントを次にレンダリングしていく。この処理は再起的に行われます。

![スクリーンショット 2022-09-29 9.20.05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/89dbb3d1-f870-35af-5cd2-979ba8380dda.jpeg)

今回の場合は親コンポーネント(Parent)でstateの更新が発生した場合、子コンポーネントA, 子コンポーネントB, 子コンポーネントC, 子コンポーネントDでもレンダリングが起こります。

コードでは下記のようになります。

```jsx:Parent.tsx
// 状態更新が発火する親コンポーネント
export const Parent = () => {
  const [count, setCount] = useState<number>(0);
  const onClick = () => {
    setCount(count + 1);
  };
  return (
    <>
      <button onClick={onClick}>+1</button>
      <ChildA />
      <ChildB />
    </>
  );
};
```

```jsx:ChildA.tsx
export const ChildA = () => {
  return (
    <>
      <p>子コンポーネントA</p>
      <ChildC />
    </>
  );
};
```

```jsx:ChildB.tsx
export const ChildB = () => {
  return (
    <>
      <p>子コンポーネントA</p>
      <ChildD />
    </>
  );
};
```

後で詳しく解説をしますが、Parentコンポーネントの``state``が更新された場合、Parentでしか使われていない(ChildAからChildDでは依存していない)のに、ChildAからChildDもレンダリング処理が実行されてしまっています。

この不要なレンダリングをなくすことでReactのパフォーマンスを最適化することができます。

#### 3. ReactがDOMに変更をコミット

コンポーネントがレンダリングをした後に、ReactはDOMへの反映を行い更新後のブラウザは画面を再描画します。

以上をまとめるとReactアプリの画面更新は下記の3ステップで行われます

- レンダリングをトリガー
- コンポーネントをレンダリング
- DOMへ変更をコミット

## State更新の回避について

[React公式ドキュメント](https://ja.reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update)より、

> 現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します。

``setState``の引数に現在の``state``と同じ値を入れた場合はレンダリングが回避されます。

コードの具体例を見てみます。

```jsx
export const Parent = () => {
  const [count, setCount] = useState<number>(0);
  const onClick = () => {
    setCount(1);
  };
  console.log("レンダリング");
  return (
    <>
      <button onClick={onClick}>+1</button>
      <p>count: {count}</p>
    </>
  );
};
```

上記のコンポーネントでは初回レンダリング及び、ボタンクリック時にcountが1に更新されるのでレンダリングが発火します。

それ以降にボタンをクリックしても、現在の``state``が1なのに対し、``setState``の引数の値も1なので現在地と更新の値が同じであることらレンダリングが発生しません。(回避される)

## Reactのパフォーマンス最適化

Reactのパフォーマンス最適化をする上で今回は下記の3つを紹介します。

- React.memo
- useCallback
- useMemo

それぞれ詳しく見ていきます。

### React.memo

React.memoは[公式ドキュメント](https://ja.reactjs.org/docs/react-api.html#reactmemo)で下記のように解説されています。

> もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。

少し噛み砕くと、``React.memo``で子コンポーネントでラップすることで、子コンポーネントで受け取る親コンポーネントからの``props``において、値の変更がなかった場合は子コンポーネントはレンダリングがスキップさせることができます。

つまり不要なレンダリングが発火せずにパフォーマンスを上げることができます。

**【React.memoの構文】**
```jsx
React.memo(メモ化したいコンポーネント);
```
親と子で異なる``count``を管理および状態更新をするようなコンポーネントを例に確認していきます。

![スクリーンショット 2022-09-30 7.19.15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/ded2e52f-8d48-a9c1-16eb-cb23ebf0df68.jpeg)
**【React.memoを使わない場合】**
```jsx:Parent.tsx
export const Parent = () => {
  const [parentCount, setParentCount] = useState<number>(0);
  const [childCount, setChildCount] = useState<number>(0);

  const addParentCount = () => {
    setParentCount(parentCount + 1);
  };
  const addChildCount = () => {
    setChildCount(childCount + 1);
  };
  return (
    <>
      <button onClick={addParentCount}>親のカウントを+1</button>
      <p>親のカウント: {parentCount}</p>
      <button onClick={addChildCount}>子のカウントを+1</button>
      <Child count={childCount} />
    </>
  );
};
```

```jsx:Child.tsx
type ChildProps = {
  count: number;
};

export const Child: React.FC<ChildProps> = ({ count }) => {
  return (
    <>
      <p>子のcount:{count}</p>
    </>
  );
};
```

子コンポーネントのカウントを増やすボタンをクリックすると、下記のように親と子の両方レンダリングされていることが確認できます。

![スクリーンショット 2022-09-30 7.39.44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/97596eec-ccfd-81ff-72c7-b412ce0358a8.jpeg)

同様に親コンポーネントのカウントを増やすボタンをクリックしてみます。すると同じく親と子の両方がレンダリングされることが確認できます。

![スクリーンショット 2022-09-30 7.41.10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/a844ee70-ee97-b8a5-52db-6c6bb9a5574e.jpeg)

ここで親コンポーネントの``parentCount``が更新された時、子コンポーネントに依存している値は更新されていないにも関わらずレンダリング処理が走ってしまっています。

この不要なレンダリングを回避するために子コンポーネントを``React.memo``でラップします。

```Child.tsx
export const Child: React.FC<ChildProps> = ({ count }) => {
  console.log("子供コンポーネントのレンダリング");
  return (
    <>
      <p>子のカウント:{count}</p>
    </>
  );
};

export const ChildMemo = React.memo(Child);
```

再度、親コンポーネントのカウントを増やすボタンをクリックすると親コンポーネントだけがレンダリングされており、値の更新がない``ChildMemo``コンポーネントはレンダリングされていないことが確認できます。

![スクリーンショット 2022-09-30 7.45.52.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/d4a3eac4-90ce-0f64-ae09-3b1ba5376fda.jpeg)

以上のように``React.memo``を利用することで、``props``で渡ってくる値に変更がない時に不用なレンダリングを回避させパフォーマンスの最適化をおこなうことができます。

### useCallback

``useCallback``は[公式ドキュメント](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)で下記のように解説されています。

> インラインのコールバックとそれが依存している値の配列を渡してください。useCallback はそのコールバックをメモ化したものを返し、その関数は依存配列の要素のいずれかが変化した場合にのみ変化します。
> これは、不必要なレンダーを避けるために（例えば shouldComponentUpdate などを使って）参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利です。

少し噛み砕くと、``useCallback``の第二引数で指定した依存配列の要素のいずれかが変化した場合のみ、メモ化した値を再計算する。

つまり依存配列の要素が変化しなかった場合は、不必要なレンダリングを避けることができる。

**【useCallbackの構文】**
```jsx
useCallback(コールバック関数, 依存配列);
```

具体的にコードを例に見ていきます。

子コンポーネントに``count``に加えて、``count``の状態を更新する(+1する)関数(``onClickChild``)を``props``で親から受け取れるようにする。

```Child.tsx
type ChildProps = {
  count: number;
  onClickChild: () => void;
};

export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
  console.log("子供コンポーネントのレンダリング");
  return (
    <>
      <button onClick={onClickChild}>子のカウントを+1</button>

      <p>子のカウント:{count}</p>
    </>
  );
};
export const ChildMemo = React.memo(Child);
```

```parent.tsx
export const Parent = () => {
  const [parentCount, setParentCount] = useState<number>(0);
  const [childCount, setChildCount] = useState<number>(0);

  const addParentCount = () => {
    setParentCount(parentCount + 1);
  };
  const addChildCount = () => {
    setChildCount(childCount + 1);
  };
  console.log("親コンポーネントのレンダリング");

  return (
    <>
      <button onClick={addParentCount}>親のカウントを+1</button>
      <p>親のカウント: {parentCount}</p>
      <ChildMemo count={childCount} onClickChild={addChildCount} />
    </>
  );
};
```

![スクリーンショット 2022-09-30 22.36.42.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/d85e49a0-f5ee-f46c-3cb5-3abd2cf6e68f.jpeg)

この状態で子のカウントを増やすボタンをクリック(``onClickChild``)すると、コンソルより親と子コンポーネントの両方がレンダリングされているのが確認できます。

![スクリーンショット 2022-09-30 22.37.10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/57f705cf-fdc2-f049-65d4-ff79d2184a39.jpeg)

次に子コンポーネントには依存していない親の``count``を更新するボタンをクリックします。

![スクリーンショット 2022-09-30 22.38.32.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/197a32f6-0be1-edef-f7c4-b2c42a9c2ce9.jpeg)

すると、先ほどコンポーネントをmemo化したのにもかかわらず子コンポーネントもレンダリングされてしまっていることが確認できます。

その理由としては、``props``で受け取っている``onClickChild``が親コンポーネントが再レンダリングされるたびに再計算され、結果として``props``で``onClickChild``が渡されるタイミングに新しい関数が渡ってきたReact側が認識し、子コンポーネントでもレンダリングが走っている。

そのため、コンポーネント本体を``React.memo``でメモ化してもレンダリングが走ってしまう。

この不要なレンダリングを回避するために、``props``で受け渡されている関数(``onClickChild``)を``useCallback``でラップしメモ化する。

```Parent.tsx
export const Parent = () => {
  const [parentCount, setParentCount] = useState<number>(0);
  const [childCount, setChildCount] = useState<number>(0);

  const addParentCount = () => {
    setParentCount(parentCount + 1);
  };
  // メモ化
  const addChildCount = useCallback(() => {
    setChildCount(childCount + 1);
  }, []);
  console.log("親コンポーネントのレンダリング");

  return (
    <>
      <button onClick={addParentCount}>親のカウントを+1</button>
      <p>親のカウント: {parentCount}</p>
      <ChildMemo count={childCount} onClickChild={addChildCount} />
    </>
  );
};
```

同様に親のカウントを増やすボタンを押すと、子のレンダリングが回避されていることが確認できます。

![スクリーンショット 2022-09-30 22.49.25.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/2f099b07-350a-2a45-3da6-1468ba7a0ee4.jpeg)

しかし、子コンポーネントのカウントを増やすボタンをクリックしても1から増えないという問題が起こってしまっています。

![スクリーンショット 2022-09-30 22.50.33.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/7f0bd6f5-f06e-155f-a863-d3844e8a234d.jpeg)

これは、先ほど定義した``useCallback``の第二引数において空配列を渡しているため、初回レンダリング時に``useCallback``でラップした関数がReact内部に保持され続けることが理由で起きている。

コード的には下記のように``setChildCount``の値が1で保持し続けられるので、1から値が更新されないようになってしまっている。

``` jsx
  const [childCount, setChildCount] = useState<number>(0);

  const addChildCount = useCallback(() => {
    // setChildCount(childCount + 1);
    // setChildCount(0 + 1);
       setChildCount(1);
  }, []);
```

これを防ぐために、``useCallback``の第二引数の依存配列に依存するstate(今回はchildCount)を入れることで、``childCount``の値が更新されたタイミングで``useCallback``の中で定義した関数が再計算される。

```jsx
  const [childCount, setChildCount] = useState<number>(0);

  const addChildCount = useCallback(() => {
    setChildCount(childCount + 1);
    // 1回目ボタンがクリックされた時
    // setChildCount(1);
    // 2回目ボタンがクリックされた時
    // setChildCount(1+1);
    // 3回目ボタンがクリックされた時
    // setChildCount(2+1);
  }, [childCount]);
```

以上のように``useCallback``を使うことで関数を``props``で受け渡す際も不要なレンダリングを回避することができる。

### useMemo

[useMemo](https://ja.reactjs.org/docs/hooks-reference.html#usememo)は公式ドキュメントでは下記のように解説されています。
> useMemo は依存配列の要素のいずれかが変化した場合にのみメモ化された値を再計算します。この最適化によりレンダー毎に高価な計算が実行されるのを避けることができます。

``React.memo``はコンポーネントを、``useCallback``はコールバック関数をメモ化していましたが、``useMemo``では計算結果の値(数値やレンダー結果)をメモ化し、不要なレンダリングを回避することができます。

**【useMemoの構文】**
```jsx
useMemo(() => メモ化したい計算ロジック, 依存配列);
```

具体的に先ほど``React.memo``を利用して``memo``化したコンポーネント内のJSXを``useMemo``を使ってメモ化していきます。

**【先ほどReact.memoでメモ化したChildコンポーネント】**
```jsx
type ChildProps = {
  count: number;
  onClickChild: () => void;
};

export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
  console.log("子供コンポーネントのレンダリング");
  return (
    <>
      <button onClick={onClickChild}>子のカウントを+1</button>
      <p>子のカウント:{count}</p>
    </>
  );
};

export const ChildMemo = React.memo(Child);
```

**【useMemoでJSXをメモ化】**
```jsx
type ChildProps = {
  count: number;
  onClickChild: () => void;
};

export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
  console.log("子供コンポーネントのレンダリング");
  return useMemo(() => {
    console.log("メモ化した値");
    return (
      <>
        <button onClick={onClickChild}>子のカウントを+1</button>
        <p>子のカウント:{count}</p>
      </>
    );
  }, [count, onClickChild]);
};
```

メモ化の確認がわかりやすいように``useMemo``の内と外に``console``を入れました。

親コンポーネントのカウント増やしてみます挙動を確認してみます。

![スクリーンショット 2022-10-03 8.23.21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/a987e9a1-dac0-8720-999f-bb11b61b45da.jpeg)

すると、メモ化されていない(``useMemo``の外側にある)``console.log("子供コンポーネントのレンダリング");``が実行されていることを確認できます。

先ほど紹介したReact.memoではコンポーネント全体をメモ化していたので、親のカウントを実行した際は子コンポーネントはレンダリングされていませんでした。

![スクリーンショット 2022-10-03 8.26.32.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/0221efee-8c80-a7cf-e8f4-87e84e5f47d5.jpeg)

次に子コンポーネントの``count``を増やすボタンをクリックしてみます。

![スクリーンショット 2022-10-03 8.27.33.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/2a9b7760-bdf1-14c9-638d-8fd69af399e3.jpeg)

すると``useMemo``でメモ化したJSXの値も実行されていることが確認できます。

## 最後に

いかがだったでしょうか。今回はReactのパフォーマンスを最適化するための基本的な方法をまとめました。

ぜひ今回紹介した手法を用いて、よりパフォーマンスがよい開発をしていただければと思います。

次回は下記のようなデータ通信におけるパフォーマンスの最適化について紹介していきたいと思っています。

- useQueryやuseSWRを利用したフェッチ処理の最適化

他にもReact周りの記事を出しているので読んでいただけると嬉しいです。

https://qiita.com/KNR109/items/e13a5c5b8b461e846902

https://qiita.com/KNR109/items/7cf6b24bed318dab5715

https://qiita.com/KNR109/items/201c133d6eeaabf675a7