【簡単】React Developer Toolsとwhy did you renderを使ったレンダリング最適化方法をいまさらだけど整理してみた

# React.memo/useCallback/useMemo...知ってはいるけどいつ使えば良いかわからない

Reactを始めてまもない方やバックエンドとフロントを両方兼務している方にとって、レンダリング最適化やパフォーマンスチューニングは苦手意識を持つ方が多いと思います。

かくいう私も普段はPHPとTypeScriptをいったりきたりしつつ、
フロント業務ではNext/Reactを触り始めてはいるもののメモ化周りとチューニング方法は全く理解できていませんでした。

<font color="Red">原因を振り返ってみると以下のようなものでした</font>
「各フックそれぞれの説明において、その場では分かった気がしていた」
「console.logが埋め込まれた前提で再レンダリングの説明をされるだけでは応用が効かない
「問題を検証　→ 分析　→ 改善（ここでメモ化周りのhooksを使う）フローで進められていない」
「そもそもどこがネックなのか検証できるツールと利用方法を理解していない」

そんな反省を踏まえて自分用の備忘録的に今回の記事を書こうと思いました。



:::note info
対象者は以下の方々を想定しています。
・React Developer Toolsをの基本的な使い方を知りたい方
・why-did-you-renderライブラリを利用した基本的な使い方を知りたい方
・Reactを触り始めたけれどReact.memo/useCallback/useMemoの使い所がわからない方

:::

※私自身、React歴はかなり浅いのでご指摘あれば是非、具体例(コード)と編集リクエストを頂けると大変嬉しく思います。

## 進め方

本記事では以下の手順で解説していきます。

1. 再レンダリングされる仕組みを再整理
2. 検証ツールの使い方
3. 簡単なケース問題

また利用しているコードは、
1.に関しては全て[codesandbox](https://codesandbox.io/)というブラウザ統合開発環境で確認できるようにしております。

2.以降に関しては[Vercel_Next.js ×Why did you render](https://github.com/vercel/next.js/tree/canary/examples/with-why-did-you-render)をベースに構築しています。
動作確認されたい方は、コピペミス防止のため私のリポジトリをクローン頂ければ早いかと思います。各章毎にブランチを切っております。

https://github.com/WebEngrChild/RenderingOptimizationWithUtilTools

## 1.再レンダリングされる仕組みを再整理

Reactにおいて再レンダリングが行われるタイミングは以下の2つです。

- stateに変化が起きた時
- 親コンポーネントが再レンダリングされた時

今後のベースとなる知識になるため、しっかり整理しておきましょう。

### stateに変化が起きた時

```tsx:App.tsx
import { useState } from "react";

export default function App() {
  console.log("stateが更新されたため再レンダリング!");
  const [text, setText] = useState("");

  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>ここに文字を入力</p>
      <input type="text" onChange={changeText} />
      <p>入力された文字が反映されます</p>
      <input type="text" value={text} />
    </>
  );
}
```

![画面収録 2022-05-25 6 53 00](https://user-images.githubusercontent.com/87892265/170138553-5608b357-196f-4f11-867e-94b801db7123.gif)

[実際にコードを動かして確認してみる](https://codesandbox.io/embed/stategeng-xin-zai-rendaringu-3y89wr?fontsize=14&hidenavigation=1&theme=dark)

input内に文字を入力すると`onChange`の`setText`によってstateが変更されます。
consoleには、stateが更新される度に出力されていることがわかります。
つまり、stateが更新される度に再レンダリングが行われています。

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

```tsx:App.tsx

import { useState } from "react";

const Child = () => {
  console.log("子が再レンダリング");

  return (
    <>
      <p>子コンポーネントが表示されています</p>
    </>
  );
};

export default function Parent() {
  console.log("親が再レンダリング!");
  const [text, setText] = useState("");

  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>親コンポーネントで文字を入力</p>
      <input type="text" onChange={changeText} />
      <Child />
    </>
  );
}

```

![画面収録 2022-05-24 7 04 47](https://user-images.githubusercontent.com/87892265/170136139-f16a6a91-884f-4c1e-83bc-1af053824a55.gif)

[実際にコードを動かして確認してみる](https://codesandbox.io/embed/qin-geng-xin-zai-rendaringu-wzwdrf?fontsize=14&hidenavigation=1&theme=dark)

上記と同様ですが、input内に文字を入力すると`onChange`の`setText`によってstateが変更されます。stateを定義しているのは親コンポーネントであるため親側では入力ごとにレンダリング＝console.logが出力されています。

一方で、子コンポーネント内ではstateは利用されていないため、再レンダリングの対象にはならないのではと思われたかもしれません。

通常のレンダリングでは、Reactは親がレンダリングされると、子コンポーネントを無条件にレンダリングします。


## 2.検証ツールの使い方

本記事では二つのツールとライブラリを用いて再レンダリングの測定を行います。

よくある説明では`console.log`をあらかじめ仕込んで再レンダリングの発生を確認するものが多いですが、実際は、検証→課題を見つける→デバッグを行うというフローが定石です。

ここでは無駄な再レンダリングが発生しているコード例をもとに、
以下を使って検証と改善を行なっていきます。
-  React Developer Tools
-  why-did-you-render


__検証ツール確認用コード__

ブランチ名：chapter_2

https://github.com/WebEngrChild/RenderingOptimizationWithUtilTools/tree/chapter_2

```tsx
import { useState } from "react";

const Child = () => {
  console.log("子が再レンダリング");

  return (
    <>
      <p>子コンポーネントが表示されています</p>
    </>
  );
};

export default function Parent() {
  console.log("親が再レンダリング!");
  const [text, setText] = useState("");

  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>親コンポーネントで文字を入力</p>
      <input type="text" onChange={changeText} />
      <Child />
    </>
  );
}
```

コード例は前章「②親コンポーネントが再レンダリングされた時」で使用したものになります。
※実際の画面や動作は既出なので割愛
※`console.log`も残してあります。

### React Developer Toolsの概要

https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja

このReactDeveloperToolsはGoogleChromeの拡張機能です。

DeveloperToolに２つcomponentタブとProfilerタブが追加されて、
以下のようなReactのコンポーネントやレンダリングの状況を検証することができます。

- コンポーネント階層構造
- Props・Stateの値
- レンダリングの可視化・回数

__初期設定__

Profilerタブ→歯車(View Settings)→Generalと進み、
Highlight updates when components renderをクリックします。

![初期設定](https://user-images.githubusercontent.com/87892265/170375836-952db3b4-673c-448d-940d-1fa586a42f3f.gif)

__検証__

![画面収録 2022-05-28 9 26 22](https://user-images.githubusercontent.com/87892265/170802524-44d54724-3747-4869-910f-c27c5c85aafd.gif)

以下の流れで操作を行なっています。

①Reload and start profilingを押す
②右上のコミットバーの数を確認
③Ranked chartを押して`Child`コンポーネントを確認
④再度Reload and start profilingを押す
⑤画面操作（文字入力）
⑥ハイライトされている（再レンダリングされいている）箇所を確認
　・Highlight updates when components renderの機能がこちらです
　・再レンダリング(文字入力)が行われる度に該当箇所が青く表示されます
⑦右上のコミットバーの数の増加を確認
　・コミット(ReactがDOMに変更を適用する）ごとにパフォーマンス情報をグループ化
　・現在選択しているコミットが青色
　・各バーの色と高さは、そのコミットのレンダリングにかかった時間を示す
⑧③を再度試し`Child`コンポーネントがレンダリングされていることを確認

__前述のフローから以下の点から無駄な再レンダリングが発生していることがわかります。__

- ⑤ ~ ⑥で文字入力が行われる度に`Child`コンポーネントもハイライトが起きている
- ⑦ ~ ⑧でも文字入力が行われている度に`Child`コンポーネントがレンダリングされている

上記を鑑みて以下のことがわかります。

・親コンポーネントのstateの更新が発生するた度に子コンポーネントで再レンダリングが発生している。
・子コンポーネントではPropsに値が渡されていないため初期レンダリング以外は親コンポーネントのレンダリングの影響を受けて欲しくない。
・子コンポーネント側で何かしらの対策（memo化）を行なえそう。

### Why Did You Renderの概要

https://www.npmjs.com/package/why-did-you-render

このライブラリを導入することで回避可能な再レンダリングが発生した場合、consoleにログを出力します。

具体的には以下のような内容です。
- 再レンダリングが発生しているComponent名
- 再レンダリングが発生している原因
- 再レンダリングのトリガーになったpropsやstateの中身

使い方としては、
①React Developer Tools等でパフォーマンスが悪い箇所を発見して当該コンポーネントに埋め込む
②新規にコンポーネント作成の際に埋め込んだ状態で開発を進めて確認する

といった使い方が想定されます。

__初期設定__

:::note warn
お手元ですぐに動作検確認するために
冒頭でも説明した通り、このライブラリーがインストール済みのプロジェクトを[Vercel](https://github.com/vercel/next.js/tree/canary/examples/with-why-did-you-render
)が公開しております。また、本記事でもこのSampleをベースに例題を作成しています。こちらをクローンしてコピペしてください。
:::

それでは、さきほどのコード「検証ツール確認用コード」の最終行のコメントアウトを解除してみてください。

```tsx
// 最終行をコメントアウトする
Child.whyDidYouRender = true
```

__検証__

先ほどと同様にい`input`欄に文字を入力してみるとconsole画面に何やら文字がたくさん表示されています。

![画面収録 2022-05-27 7 37 07](https://user-images.githubusercontent.com/87892265/170590982-921e3b43-3110-480f-9c00-6c9ec20c7603.gif)

```console
Re-rendered because the props object itself changed but its values are all equal.' 
```

```console
Rendered by Parentfather from re-rendering.' 
[hook useState result]
{"prev ": ''} '!==' {"next ": 'a'}
```

__このアラートから以下のことが読み取れます。__

- 値は等しいけれどpropsオブジェクト自体が更新されたため、Childコンポーネントは再レンダリングしている
- 親が再レンダリングされたことによって再レンダリングしている
- useStateの値が` `から`a`に変わったことが要因

### 改善（React.memoでメモ化を行う）

ブランチ名：chapter_2_refactoring

https://github.com/WebEngrChild/RenderingOptimizationWithUtilTools/tree/chapter_2_refactoring

React.memoとはコンポーネント（コンポーネントのレンダリング結果）をメモ化するReactのAPI（メソッド）になります。

親から受け取るPropsの等価性＝変化があるかをモニタリングしており、propsに変化がなければコンポーネントの再レンダリングをスキップすることができます。

先に示したサンプル例を以下のようにReact.memoラップしてあげます。

```tsx
import React from "react";
import { useState } from "react";

const Child = React.memo(() => {
  console.log("子が再レンダリング");

  return (
      <>
        <p>子コンポーネントが表示されています</p>
      </>
  );
});

// ①esLintエラー対策
Child.displayName = 'Child';

export default function Parent() {
  console.log("親が再レンダリング!");
  const [text, setText] = useState("");

  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  return (
// ②Reactフラグメントを削除してdivタグを追加
    <div>
      <p>親コンポーネントで文字を入力</p>
      <input type="text" onChange={changeText} />
      <Child />
    </div>
  );
}

// whyDidYouRenderで利用します
Child.whyDidYouRender = true
```

__補足__

①React.memoを利用するとdisplayNameを検知できなくなるため改めて明示的に定義する必要があります。未定義だとeslintによるエラーが表示されます。

②Reactフラグメントがあると子コンポーネントをメモ化しているのにも関わらず、
React Developer ToolsのHighlight updates when components render機能で確認しても再レンダリングされているように表示されてしまいます。

対応としてはFragment箇所を削除、`<div>`タグ等に変換することで対応できます。

### 再検証

それでは修正した結果でそれぞれの検証ツールでどのような変化があったのか確認します。

__React Developer Toolsの再検証__

![画面収録 2022-05-28 9 38 53](https://user-images.githubusercontent.com/87892265/170802942-fa648032-f142-43e6-a7bc-706769c1298d.gif)

修正前は文字入力（再レンダリング）が行われる度に左側のハイライトがされていた箇所が少なくなっていることがわかります。

また、コミットごとに表示されていた`Child`コンポーネントの表示数も無くなりました。

__Why Did You Renderの再検証__

![画面収録 2022-05-28 10 05 04](https://user-images.githubusercontent.com/87892265/170803751-9f52d013-78ef-49e5-82a8-c3e801517718.gif)

こちらは非常にわかりやすくmemo化したことにより`console`に表示されていたエラーが消えています。

## 3.簡単なケース問題

こちらでは`React.memo`　`useCallback`　`useMemo`３つを利用してレンダリング最適化を行います。

これまでと同様以下の流れで進めます。

1. 無駄なレンダリング・パフォーマンスが悪いコード例
2. 検証ツールを用いて問題箇所を計測する
2-1. React Developer Tools
2-2. why-did-you-render
3. コード修正
4. 再検証

__無駄なレンダリング・パフォーマンスが悪いコード例__

ブランチ名：chapter_3

https://github.com/WebEngrChild/RenderingOptimizationWithUtilTools/tree/chapter_3

```tsx
import React from "react";
import { useState } from "react";

// ①Propsの受け取りはない子コンポーネント
const Child_1 = () => {
  return (
    <p>Child_1コンポーネント</p>
  );
};

// ②callback関数をPropsで受け取っている子コンポーネント
const Child_2 = (props: { handleClick: () => void }) => {
  return (
    <p>Child_2コンポーネント</p>
// <button onClick={props.handleClick}>Child_2コンポーネント</button>
  );
};

export default function Parent() {
  const [text, setText] = useState("");
  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const handleClick = () => {
    console.log("click");
  };


  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

// ③計算に時間が掛かる重い処理結果を格納している値
  const doubledCount = double(count);

  return (
    <div>
      <p>親コンポーネントで文字を入力</p>
      <input type="text" onChange={changeText} />
      <Child_1 />
      <Child_2 handleClick={handleClick} />
      <p>親コンポーネント側での重い処理</p>
      <p>
        Counter: {count}, {doubledCount}
      </p>
      <button onClick={() => setCount(count + 1)}>Increment count2</button>
    </div>
  );
}

Child_1.whyDidYouRender = true;
Child_2.whyDidYouRender = true;
```

①`Child_1`はPropsの受け渡しはないシンプルなコンポーネントです。
②`Child_2`は親コンポーネントからcallback関数を受け取り、子コンポーネント内で利用しています。
③は親コンポーネントで利用している値になります。計算するの非常に時間が掛かる重い処理になります。

### 検証ツールを用いて問題箇所を計測する
__React Developer Tools__

![画面収録 2022-05-28 14 41 04](https://user-images.githubusercontent.com/87892265/170811888-e295eb71-1f90-43b3-8547-5690ecf230ac.gif)

実際にお手元で動かしてみれば分かりますがinput項目に文字を一文字入力するだけでも非常に重い動きになっています。

Ranked chartをでも`Parent`コンポーネントが圧倒的にパフォーマンスのボトルネックになっていることがわかります。

さらに`Parent`コンポーネントが再レンダリングされる度に`Child_1` `Child_2`も合わせてレンダリングされており無駄が発生していそうです。

まとめると以下の仮説が立ちました。

- 親コンポーネント側での処理がパフォーマンスのボトルネックになっている。
- 親コンポーネントの再レンダリングに関係のない子コンポーネントも影響されてしまっている

__why-did-you-render__

React Developer Toolsで立てた仮説を同ライブラリを用いてさらに検証してみます。

![画面収録 2022-05-28 14 50 39](https://user-images.githubusercontent.com/87892265/170812147-40a7b26a-84d5-42d9-9bd9-a0da928f502f.gif)

consoleに出力されている内容を整理すると以下の通りです。

__Child_1__
- 親コンポーネントでの`useState`の値が からaに変わったことが要因変更されたことによって再レンダリングした
- propsの値自体は変わっていない

__Child_2__
再レンダリング理由は以下の二つ
- 親コンポーネントでの`useState`の値が` `からaに変わった
- propsの値である`handleClick`が変化した


### コード修正

ブランチ名：chapter_3_refactoring

https://github.com/WebEngrChild/RenderingOptimizationWithUtilTools/tree/chapter_3_refactoring

```tsx
import React from "react";
import { useState, useCallback, useMemo } from "react";

// ①コンポーネントをメモ化する
const Child_1 = React.memo(() => {
  return (
    <p>Child_1コンポーネント</p>
  );
});
const Child_2 = React.memo((props: { handleClick: () => void }) => {
  return (
    <p>Child_2コンポーネント</p>
    // <button onClick={props.handleClick}>Child_2コンポーネント</button>
  );
});

export default function Parent() {
  const [text, setText] = useState("");
  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

// ②コールバック関数のメモ化
  const handleClick = useCallback(() => {
    console.log("click");
  },[]);

// ③重い処理をメモ化
  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };
  const doubledCount = useMemo(() => double(count),[count]);

  return (
    <div>
      <p>親コンポーネントで文字を入力</p>
      <input type="text" onChange={changeText} />
      <Child_1 />
      <Child_2 handleClick={handleClick} />
      <p>重い処理</p>
      <p>
        Counter: {count}, {doubledCount}
      </p>
      <button onClick={() => setCount(count + 1)}>Increment count2</button>
    </div>
  );
}

Child_1.displayName = 'Child_1';
Child_2.displayName = 'Child_2';

Child_1.whyDidYouRender = true;
Child_2.whyDidYouRender = true;
```

__親子間の不要な再レンダリングを防ぐ__
①検証時で明らかになったように`Child_1` `Child_2`コンポーネントは親に影響されて無駄な再レンダリングが発生していました。

`React.memo`を使いPropsの値が更新される時以外のレンダリングをスキップするようにします。

②関数はコンポーネントが再レンダリングされる度に再生成されてしまいます。

関数の処理が同じでも、新しいhandleClickと前回のhandleClickは異なるオブジェクトなので等価ではありません。そのため、コンポーネントが再レンダリングされてしまいます。

そこで`useCallback`を利用して関数をメモ化します。

__親側の処理を軽くする__
③`doubledCount`は一定時間要するループ後に`count`を２倍した結果が格納されます。本来であれば以下のボタンがクリックされて`count`が更新される時のみ処理が実行してほしいはずです。

```tsx
<button onClick={() => setCount(count + 1)}>Increment count2</button>
```

`useMemo`を利用することで、引数の依存配列で指定した`count`に変化が起きる時以外はメモ化された値を利用するようにします。

### 再検証

__React Developer Tools__

![画面収録 2022-05-28 15 06 40](https://user-images.githubusercontent.com/87892265/170812628-07993ae1-ec0d-4152-bc33-ff978ba7f335.gif)

__親子間の不要な再レンダリングを防げた！！__
`Child_1` `Child_2` コンポーネントのレンダリングが減っていることがわかります。

__親側の処理を軽くできた！！__
初回の`Parent`コンポーネントのレンダリング以降はメモ化された値を利用しているためレンダリングに掛かる時間が大幅に削減されていることがわかります。

__why-did-you-render__

![画面収録 2022-05-28 15 11 22](https://user-images.githubusercontent.com/87892265/170812748-ce43bf57-58dd-4cbf-8afd-c48228456c15.gif)

親側でinput入力を行なっても（stateを更新）、consoleエラーが表示されなくなりました。

## 参考記事

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

https://tmegos.hatenablog.jp/entry/react-re-render-why-did-you-render

https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2#%E3%81%AA%E3%81%9C-usecallback-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B