[React Hook] レンダリングのチューニング ~オブジェクトをContextとして利用する場合 & サンプルコード~

概要

この記事について

かんたんな概要と結論

オブジェクトをContextとして使用する際のレンダリングのチューニングを実行した。
外部ライブラリのisDeepEqualuseDeepCompareEffectを使用することで
良い感じにDeep Compareを実装できる。

React Hookによるブラウザアプリ画面の作成の際、
レンダリングされるComponentの数を絞る(チューニングする)ことで
実行されるコードのステップ数を減らし、パフォーマンスを改善することができる。

オブジェクトをContextとして各Componentにわたす処理のコードにおいて、
いくつかの観点ごとにチューニングする必要があったため、
ここにそれを記したい。

レンダリングのチューニング

やりたいこと

React HookにおいてオブジェクトをContextとして各Componentにわたす際に、
オブジェクトを更新するたびに、通常はContextを使用するすべてのComponentが再レンダリングされる。

オブジェクトが再度同じ値に設定される場合に、
不要なレンダリングを防ぐような仕組みが、Reactの標準APIにないかと探したが、
どうやら存在しないようだった。

また、もう一つの課題として、
利用先のComponentContextのオブジェクトを取得し、
その値(参照ではなく、内容)に変更があるたびにuseEffectを実行させたいというものがある。
useEffectはimmutableな値のみを依存関係に含めることができるので、
今回のようなオブジェクトの変更を察知することが難しい。

上記の2点について解決したい。

改善観点

  • レンダリングされるComponentの数をチューニング
  • useEffectの実行タイミングの調整

初期状態(サンプルデモ)

初期表示、およびSetterBoxButtonを二回押下した際のログは次のようになる。

 1render Container          //*** 初期表示ここから
 2render Box
 3render EffectBox
 4render SetterBox
 5run useDeepCompareEffect
 6{foo: {…}, bar: {…}}      //*** ここまで
 7render Container          //*** 一回目ここから
 8render Box
 9render EffectBox
10render SetterBox          //*** ここまで
11render Container          //*** 二回目ここから
12render Box
13render EffectBox
14render SetterBox          //*** ここまで
15
  • ボタン押下ごとにContainer Componentおよびすべての子コンポーネントが再レンダリングされている。
  • EffectBoxuseEffectが一回目のボタン押下時に実行されない。
    (依存関係に空の配列[]を渡しているためにそうなる。本来ならボタン押下でmyContextが変更した際に実行してほしい)

チューニングの実行

レンダリング対象の絞り込み

i. React.memoを使用

Containerの子コンポーネントのそれぞれを、
React.memoを用いてメモ化する。

引数の値が変更しなければ、再レンダリングを引き起こさないようにする。

ログは次のようになる。

 1render Container         //*** 初期表示ここから                
 2render Box 
 3render EffectBox
 4render SetterBox
 5run useDeepCompareEffect
 6{foo: {…}, bar: {…}}     //*** ここまで
 7render Container         //*** 一回目ボタン押下 ここから
 8render Box
 9render EffectBox         //*** ここまで
10render Container         //*** 二回目ボタン押下 ここから
11render Box
12render EffectBox         //*** ここまで
13  

子コンポーネントのうち、
SetterBoxは再レンダされなくなった。

しかし、
BoxおよびEffectBoxはメモ化しても再レンダされる。

ボタン押下時にmyContextが更新され、myContext.Providerで渡される値が変化するため、
useContext(myContext)によってmyContextを使用しているComponentはすべて再レンダされる。
ここで渡される値はオブジェクトの参照値なので、たとえ内容が等しくても「別のオブジェクト」と判定され、
レンダリングが実行される。

次のiiで、その挙動を改善したい。

ii. useRefを使用してDeep Compareを実行

Container Componentcxt変数が更新されたあと、
useRefでフックされたcxtRef変数と、
Deep Equal(ネストされた各値のすべてが等しいことを保証)によって内容を比較する。
myContext.Providerのvalueに渡されるのはcxtではなくcxtRef.currentとなる。

Deep Equalの実装には、外部ライブラリのfast-deep-equalを使用する。

ログは次のようになる。

 1render Container             //*** 初期表示ここから
 2render Box
 3render EffectBox
 4render SetterBox
 5run useDeepCompareEffect
 6{foo: {…}, bar: {…}}       //*** ここまで
 7render Container             //*** 一回目ボタン押下 ここから
 8render Box
 9render EffectBox             //*** ここまで
10render Container             //*** 二回目ボタン押下 ここからここまで
11  

二回目のボタン押下では、
myContextの内容は変更しないため、
Deep Compareの結果cxtRef.current変数の参照は変化しない。

したがって、BoxおよびEffectBoxの再レンダは引き起こされない。

useEffectの実行タイミングの調整

useEffectの依存関係においてmutableな値を許容するトリックだが、
検索した結果useDeepCompareEffectという便利なフックが公開されていた。

こちらの記事の下の方に説明が記載されています。

これを用いることによって、
myContextが変更したタイミングのいずれにおいても
EffectBoxのサイドエフェクト処理が走るようにできる。

ログは次のようになる。

 1render Container             //*** 初期表示ここから
 2render Box
 3render EffectBox
 4render SetterBox
 5run useDeepCompareEffect
 6{foo: {…}, bar: {…}}         //*** ここまで
 7render Container             //*** 一回目ボタン押下 ここから
 8render Box
 9render EffectBox             
10run useDeepCompareEffect
11{foo: {…}, bar: {…}}      //*** ここまで
12render Container             //*** 二回目ボタン押下 ここからここまで
13  

一回目のボタン押下でmyContextの内容が変更されるため、
EffectBox内のサイドエフェクト処理が実行され、ログに出力されるようになった。

関連記事

comments powered by Disqus