Reactアプリケーションを高速化するための方法
結論から
無駄な再レンダリングを発生させない
処理しないこと
高速化とは処理しないことです
矛盾しているように思えますが、処理が減ればアプリケーションは高速化します
Reactの処理を減らすためには
再レンダリングをしないことです
もちろん完全に再レンダリング無しというのは不可能なので、いかにそれを減らしていくかが重要です
再レンダリングの妥当性
妥当な再レンダリング
コンポーネントのstateに変化があって自身の挙動を変更しなければならないとしたら、その再レンダリングは妥当です
妥当ではない再レンダリング
コンポーネント自身がそのstateを必要としておらず、下位に配るためstateを保持しているとしたら、その際レンダリングは妥当ではありません
そういう構造を作ってしまえば、配下にいる全コンポーネントがstateと無関係に再レンダリングされます。
何故、こんな構造がまかり通ってしまうのか
useStateが三つの機能を一つに集約してしまったのが原因です。詳しくは[React]useStateによるstate管理の問題点とその解決方法を模索した結果、3つの機能を分離するに至った話を参照してください。
解決方法
上位コンポーネントで下位に配るためのstateを保持しない
無駄なstateの保持、これが諸悪の根源です
とにかく自身で使用しないstateを保持するのをやめる必要があります
上位コンポーネントでstateを保持しないためには
state管理ライブラリを使用する
ReduxやContextAPIを使用すれば、親コンポーネントを経由せずともstateを配ることが出来ます
ただし、以下の欠点があります
- stateのスコープがグローバルになってしまい、特定のコンポーネントグループ内で閉じさせるのが困難
- 仕組みを作成するコストが大きく、気軽に利用しにくい
state管理をコンポーネントグループでローカル化する
特定コンポーネント間で閉じられたstate管理が出来て、簡単に利用出来れば問題は解決です
ということで作りました
- stateの作成、更新、取得を分離させたライブラリ
@react-libraries/use-local-state - コンポーネント間でイベント通知を行うライブラリ
@react-libraries/use-local-event
両方とも、stateやeventを扱うための識別ハンドルを上位コンポーネントで作成し、それを下位コンポーネントで使用する仕組みです
ハンドルを作成した上位コンポーネントはstateの更新やevent通知の影響を受けないので、再レンダリングはされません
state管理のグループローカル化の具体例
Sample1 特に対策を打たない、通常の書き方
Inputコンポーネントにボタンが配置されており、押すごとにstateが+1されます
Test1とTest2はstateを受け取って、値を1万回表示します
Test3はstateとは無関係にTest3を1万回表示します
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化したものです
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によって、上位コンポーネントの再レンダリングを行わない書き方になっています
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
特に何も対応していないので、全てのコンポーネントが再レンダリングされています
当然、一番遅いです
Sample2の結果
レンダリング時間 162.2ms
memo化によってTest3の更新はスキップされていますが、Appは再レンダリングされています
何もしないよりは速度が向上していますが、まだ無駄が残っています
Sample3の結果
レンダリング時間 95.2ms
本当に更新が必要なコンポーネントのみ再レンダリングされています
処理を行う内容は必要最低限です
まとめ
Reactアプリケーションを高速化させるためには、更新の必要が無いコンポーネントを再レンダリングしないことです。そのためにはとにかく上位コンポーネントに余計なstateを持たせないというのが基本です
今回は効率的なstate管理のために、stateをグループで閉じるためのライブラリを使用しました。その他にstateを使わずにevventのみ配るライブラリもあるので、次回はそちらも紹介したいと思います