Reactアプリケーションを高速化するための方法

# Reactアプリケーションを高速化するための方法

## 結論から

無駄な再レンダリングを発生させない

### 処理しないこと

高速化とは処理しないことです
矛盾しているように思えますが、処理が減ればアプリケーションは高速化します

### Reactの処理を減らすためには

再レンダリングをしないことです
もちろん完全に再レンダリング無しというのは不可能なので、いかにそれを減らしていくかが重要です


## 再レンダリングの妥当性

### 妥当な再レンダリング

コンポーネントのstateに変化があって自身の挙動を変更しなければならないとしたら、その再レンダリングは妥当です

### 妥当ではない再レンダリング

コンポーネント自身がそのstateを必要としておらず、下位に配るためstateを保持しているとしたら、その際レンダリングは妥当ではありません
そういう構造を作ってしまえば、配下にいる全コンポーネントがstateと無関係に再レンダリングされます。

### 何故、こんな構造がまかり通ってしまうのか

useStateが三つの機能を一つに集約してしまったのが原因です。詳しくは[[React]useStateによるstate管理の問題点とその解決方法を模索した結果、3つの機能を分離するに至った話](https://qiita.com/SoraKumo/items/6267133940c15c89481e)を参照してください。

## 解決方法

### 上位コンポーネントで下位に配るためのstateを保持しない

無駄なstateの保持、これが諸悪の根源です
とにかく自身で使用しないstateを保持するのをやめる必要があります


### 上位コンポーネントでstateを保持しないためには

#### state管理ライブラリを使用する

ReduxやContextAPIを使用すれば、親コンポーネントを経由せずともstateを配ることが出来ます
ただし、以下の欠点があります

- stateのスコープがグローバルになってしまい、特定のコンポーネントグループ内で閉じさせるのが困難
- 仕組みを作成するコストが大きく、気軽に利用しにくい

#### state管理をコンポーネントグループでローカル化する

特定コンポーネント間で閉じられたstate管理が出来て、簡単に利用出来れば問題は解決です
ということで作りました

- stateの作成、更新、取得を分離させたライブラリ  
[@react-libraries/use-local-state](https://www.npmjs.com/package/@react-libraries/use-local-state)
- コンポーネント間でイベント通知を行うライブラリ  
[@react-libraries/use-local-event](https://www.npmjs.com/package/@react-libraries/use-local-event)

両方とも、stateやeventを扱うための識別ハンドルを上位コンポーネントで作成し、それを下位コンポーネントで使用する仕組みです
ハンドルを作成した上位コンポーネントはstateの更新やevent通知の影響を受けないので、再レンダリングはされません

## state管理のグループローカル化の具体例

### Sample1 特に対策を打たない、通常の書き方

Inputコンポーネントにボタンが配置されており、押すごとにstateが+1されます
Test1とTest2はstateを受け取って、値を1万回表示します
Test3はstateとは無関係にTest3を1万回表示します

```tsx
import React, { useState } from 'react';

const count = 10000;

const Test1 = ({ value }: { value: number }) => (
  <div>
    <div>Test1</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
);
const Test2 = ({ value }: { value: number }) => (
  <div>
    <div>Test2</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
);
const Test3 = () => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
);
const Input = ({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => (
  <button onClick={() => setValue((v) => v + 1)}>+1</button>
);

const App = () => {
  const [value, setValue] = useState(0);
  return (
    <>
      <div>通常</div>
      <Input setValue={setValue} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 value={value} />
        <Test2 value={value} />
        <Test3 />
      </div>
    </>
  );
};
export default App;
```

### Sample2 memo化

Sample1をmemo化したものです

```tsx
import React, { memo, useState } from 'react';

const count = 10000;

const Test1 = memo(({ value }: { value: number }) => (
  <div>
    <div>Test1</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
));
const Test2 = memo(({ value }: { value: number }) => (
  <div>
    <div>Test2</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>{value}</div>
    ))}
  </div>
));
const Test3 = memo(() => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
));
const Input = memo(({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => (
  <button onClick={() => setValue((v) => v + 1)}>+1</button>
));

const App = () => {
  const [value, setValue] = useState(0);
  return (
    <>
      <div>memo化</div>
      <Input setValue={setValue} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 value={value} />
        <Test2 value={value} />
        <Test3 />
      </div>
    </>
  );
};
export default App;
```

### Sample3 グループローカル化

useLocalStateによって、上位コンポーネントの再レンダリングを行わない書き方になっています

```tsx
import {
  LocalState,
  mutateLocalState,
  useLocalState,
  useLocalStateCreate,
} from '@react-libraries/use-local-state';
import React from 'react';

const count = 10000;

const Test1 = ({ state }: { state: LocalState<number> }) => {
  const [value] = useLocalState(state);
  return (
    <div>
      <div>Test1</div>
      {new Array(count).fill(0).map((_, index) => (
        <div key={index}>{value}</div>
      ))}
    </div>
  );
};
const Test2 = ({ state }: { state: LocalState<number> }) => {
  const [value] = useLocalState(state);
  return (
    <div>
      <div>Test2</div>
      {new Array(count).fill(0).map((_, index) => (
        <div key={index}>{value}</div>
      ))}
    </div>
  );
};
const Test3 = () => (
  <div>
    <div>Test3</div>
    {new Array(count).fill(0).map((_, index) => (
      <div key={index}>Test3</div>
    ))}
  </div>
);
const Input = ({ state }: { state: LocalState<number> }) => (
  <button onClick={() => mutateLocalState(state, (v) => v + 1)}>+1</button>
);

const App = () => {
  const state = useLocalStateCreate(0);
  return (
    <>
      <div>localState</div>
      <Input state={state} />
      <hr />
      <div style={{ display: 'flex' }}>
        <Test1 state={state} />
        <Test2 state={state} />
        <Test3 />
      </div>
    </>
  );
};
export default App;
```

### 結果の検証

ボタンを押してstateを変化させたときのprofile結果です
`Render duration`がレンダリング時間です

#### Sample1の結果

レンダリング時間 228.2ms

特に何も対応していないので、全てのコンポーネントが再レンダリングされています
当然、一番遅いです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/dc47e4e4-96e4-f46d-8be8-98d0990128e5.png)

#### Sample2の結果

レンダリング時間 162.2ms

memo化によってTest3の更新はスキップされていますが、Appは再レンダリングされています
何もしないよりは速度が向上していますが、まだ無駄が残っています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/755b4dd9-b64f-9cfc-91f8-7ebf40cc9e27.png)

#### Sample3の結果

レンダリング時間 95.2ms

本当に更新が必要なコンポーネントのみ再レンダリングされています
処理を行う内容は必要最低限です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/6ac5919f-1a6d-54fb-7cc2-3a4b792666a3.png)

## まとめ

Reactアプリケーションを高速化させるためには、更新の必要が無いコンポーネントを再レンダリングしないことです。そのためにはとにかく上位コンポーネントに余計なstateを持たせないというのが基本です

今回は効率的なstate管理のために、stateをグループで閉じるためのライブラリを使用しました。その他にstateを使わずにevventのみ配るライブラリもあるので、次回はそちらも紹介したいと思います