Reactのパフォーマンス改善対策には、主に2つあります。
1.サーバーからのデータ取得回数を減らす
キャッシュをすることが重要。従来のuseContextやReduxはキャッシュのメカニズムを持っていない。
近年登場したサーバーキャッシュマネジメントツールによってサーバーから取得したデータをキャッシュすることができるようになりました。具体的には以下のようなツールがあります。
React Query
通常のReactアプリを作成する場合はこちらを使います。
useSWR
Next.jsを使う場合はこちらが相性が良いです。
RTK Query
スター数が少なくまだ開発中ですが選択肢としてはありです。
2.不要な再レンダリングを抑制すること
useContextを使うと再レンダリングによるパフォーマンスの低下がよく発生する。対策にも工数がかかる。
ReduxかRedux Tool Kitを使うようにすると対策が楽です。
レンダリングされるタイミング
初期のローディング
当たり前ですが、まずは必ずこれが行われます。
useStateの値が更新された時
自身のコンポーネントのuseStateはもちろんそうですが、親で持っているコンポーネントが更新されると子のコンポーネントもすべて再レンダリングが行われることになります。
例えば、taskなどサーバーから取得するstateだけでなく、isLoadingやisErrorなどよく使われる汎用的なステートの更新でも再レンダリングが動いてしまうので注意です。
また、例えば、isLoadingをtrueにして、falseにするという動作をした場合はそれだけでプラス2回レンダリングが行われることになるので注意が必要です。
対策
React Queryによる対策
これもReact Queryで対策することもできます。reactQueryの返却ステータスとしてloadingやerrorなどの状態を保持してくれているのでReact側で別途loadingのステータスを持つ必要がないためです。
useContextの再レンダリング対策(非推奨)
useContextのProviderで全然関係ないコンポーネント同士(コンポーネントA、コンポーネントB)のステートを持たせておいてコンポーネントAに関係するステートを更新したら、コンポーネントBのレンダリングも行われてしまうことになります。全体のレンダリングの総回数も増えることになります。
そこで、対策方法としては使うコンポーネントごとにProviderを分けるという方法があります。
ただ、その場合厳密にレンダリング回数を抑制しようとすると、useStateの変数と関数をさらに別々にProviderに分けなければならなかったりして非常に冗長な構成になってしまいます。Providerの数が増えれば、保守性や開発効率が大幅に下がるのであまり推奨はされていないです。
Reduxのconnectによる対策(現時点では下火)
connectは、useContextと違ってReact外でstateを管理するのでContextを使ったときのようにすべてのContextに再レンダリングが影響するということはないです。また、pureComponentやReact.memoと同じように動作するので使うだけで親子間の再レンダリングの対策ができていました。
しかし、現時点ではコードの書きやすさを重視してHooksのuseSelectorを使うことが推奨されています。
useSelector + React.memoによる対策(現時点では推奨)
useSelectorもuseContextと違ってReact外でstateを管理するのでContextを使ったときのようにすべてのContextに再レンダリングが影響するということはないです。しかし、useSelectorはあくまでフックなので、親コンポーネントと子コンポーネントの関係の再レンダリングの抑制はしてくれません。(デフォルトだと、親がレンダリングされたら、例えある子コンポーネントには関係ないstateの更新だとしてもすべての子コンポーネントが再レンダリングされてしまう。)
その場合は、memoなどを使って子コンポーネントをmemo化すれば、子が再レンダリングされることはなくなる。コンポーネントのmemo化に関しては下記の記事で解説しています。
【React】「React.memo」について
3.再計算を抑制すること
reselect(Redux)
ReduxからuseSelectorで値を取得する処理自体もfilterなどで処理コストがかかっている場合はreselectを使ってメモ化します。
useMemo
reselectで取得したデータをさらに加工する場合や、各コンポーネントに固有で持たせるデータなどをメモ化する場合などに使います。なお、useMemo自体にもコストはかかるので全ての関数につけるということは避けましょう。
- Storeから取得したデータをmapやfilterでループさせる処理
useCallback
React.memoの引数で関数をメモ化したい場合に使う。
アロー関数で書いた関数はレンダリングのたびに毎回違う関数を生成します。なので、propsで関数を渡した際に毎回値が変わっていると判定されてしまって、memo化したとしても再レンダリングが行われることになります。
なお、useCallback自体にもコストはかかるので全ての関数につけるということは避けましょう。
参考:Reactのレンダリングの流れ
レンダーフェーズ
- ルートのコンポーネントから更新が必要なコンポーネントを見つけるために下方にループします。(更新が必要なコンポーネントはフラグが立っています。)
- 更新が必要なコンポーネントをレンダリング出力(JSX)を収集する。
- Reactはオブジェクトの新しいツリー(仮想DOM)の差分を計算する。
コミットフェーズ
- 計算した結果をDOMに適応する。
その後
- componentDidMountやcomponentDidUpdateのライフサイクルメソッドを実行する。
- その後、useEffectフックを実行する。
Reactのレンダリング動作詳細
親コンポーネントがレンダリングされるとつられて子コンポーネントも全てレンダリングする。
実際は、ほとんどのDOMに差がないので、Reactはレンダリングではなく差分を計算する作業を行う動作となる。
Reactにレンダリング指示をする方法
クラスコンポーネント
- this.setState()
- this.forceUpdate()
関数コンポーネント
- useStateのセッター
- useReducerのdispatch
その他
- ReactDom.render(<App>)を再度呼び出す。
Reactのコンポーネントレンダリングをスキップするための方法
これらのすべては「Shallow Equality」という比較テクニックを使っています。
shouldComponentUpdate
クラスコンポーネントのライフサイクルメソッドの一つ。これをfalseを返せばいける。
PureComponent
shouldComponentUpdateをデフォルトで実装してくれている。
React.memo
HOCです。コンポーネントを引数として受け取って新しいラッパーコンポーネントを返します。
この記事へのコメントはありません。