30歳からのプログラミングスマートフォン用の表示で見る30歳からのプログラミング30歳無職から独学でプログラミングを開始した人間の記録。2019の20日目の記事です。パフォーマンス・チューニングには、「こうすれば必ず上手くいく」という方法論や銀の弾丸はなく、地道に試行と計測を繰り返すしかない。基本的な考え方や仕組みを理解することが大切であり、それがなければ、どのように対処していけばいいのか見当をつけることすら出来ず、的外れな対応をすることにもなりかねない。アプリのパフォーマンス改善のための、基本となるテクニックのひとつである。v16.10.2で動作確認している。アプリのパフォーマンス最適化を理解するためにはまず、メモ化(Memoization)という概念を把握しておく必要がある。大雑把に言ってしまうとメモ化とは、何らかの計算によって得られた値を記録しておき、その値が再度必要になったときに、再計算することなく値を得られるようにすることである。メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。アプリのパフォーマンス最適化の基本的な戦略である。おいてはメモ化にはもうひとつ重要な役割がある。それは、同じ参照のオブジェクトを得られるようになるということ。記録しておいたオブジェクトを取り出すので、「値が一緒だが参照が異なる別のオブジェクト」になってしまうことを防げる。例外ではないため、これは重要な意味を持つ。後述するように、React.memoを上手く使うためにはこの効果も把握しておく必要がある。再レンダーによるコストコンポーネントのstateが更新されると、そのコンポーネントは再レンダーされる。親コンポーネントが再レンダーされると、その子コンポーネントも無条件で再レンダーされる。下記の例では、ボタンを押す度にAppとChildが再レンダーされるため、その度にログが流れる。App;再レンダーには当然、それを実行するためのコストがある。上記のサンプルではconsole.logを実行しているが、この部分で高コストな処理を行っていれば、その分だけパフォーマンスに悪影響を与える。内部的に行う処理もある。更新する必要があるかをチェックしている。これらの処理にも当然、少なからずコストが掛かっている。上記のChildは、stateが何であれレンダーする内容は変わらないため、Childの再レンダーが不要であることは一目瞭然。そのため再レンダーをスキップできれば、その分だけ、パフォーマンスの向上が期待できる。それを実現するために利用するのがメモ化であり、その具体的な方法がReact.memoである。要素を再利用することで、再レンダーを最低限に抑える。再レンダーが必要かどうかは、propsの等価性によって判断される。新しく渡されたpropsを前回のpropsと比較し、同じであると見なせば再レンダーは不要であると判断し、異なると見なせば再レンダーを行う。compareを使う。これは、オブジェクトの1階層のみを比較することである。compareによって等価であると判断される。React.memoを使ってみる。React.memoは、メモ化したいコンポーネントをラップして使う。そのため、Childを以下のように書き換える。<div>Child</div>;});これで、Childの再レンダーは常にスキップされるようになった。compareでは再レンダーをスキップできないパターンを見てみる。stateをオブジェクトにして、そのなかにappとchildというプロパティを持たせた。どちらもボタンを押して個別にカウントアップできる。Childコンポーネントはstateを受け取り、state.childを画面に表示している。childボタンが押したときだけ再レンダーすれば、十分なはず。appボタンを押したときも再レンダーされてしまう。原因は、Childにstateというオブジェクトを渡してしまったこと。compareすると、前回のprops.stateと今回のprops.stateは常に違うオブジェクトになるので、等価性がないと判断される。そのため、必ず再レンダーされてしまう。この問題の正しい解決法は、Childにstateを渡すのを止め、state.childだけを受け渡すようにすることである。Childはstate.appを必要としていないのだから、stateオブジェクトを丸ごと渡しているのが、そもそも間違っている。今回はReact.memoの説明のため、敢えて違う方法で解決する。React.memoの第二引数には関数を渡すことができ、公式ドキュメントではこの関数のことをareEqualと呼んでいる。areEqualは、第一引数として前回のprops(prevProps)を、第二引数として今回のprops(nextProps)を受け取る。areEqualは、真偽値を返すように書く必要がある。そのため、このような構文になる。*/})areEqualがtrueを返したときは再レンダーをスキップし、falseを返したときは再レンダーを行う。compareで等価性を判断する。そのため、Childを以下のように書き換えると、state.childの値が変化していないときは再レンダーを不要と判断し、スキップする。nextProps.state.child);繰り返しになるが、今回のケースでは、propsとしてstate.childだけを渡す、というのが正しい解決方法である。パフォーマンスの問題以前にコンポーネントの設計として、不必要なpropsを渡すべきではない。今回は説明のしやすさのために、敢えてこのようなサンプルにした。areEqualで再レンダーをコントロールできるので、これを使えば、表示を制御することが可能になる。データの変化を表示に反映させたい場合にのみ、areEqualがfalseを返すようにすればよい。そのような使い方をしてはいけない。公式ドキュメントにあるように、React.memoはパフォーマンス最適化のためにのみ、使う。これはパフォーマンス最適化のためだけの方法です。バグを引き起こす可能性があるため、レンダーを「抑止する」ために使用しないでください。Reactもうひとつ注意しなければならないのは、等価性のチェックにも、当然コストがかかるということ。areEqualで複雑なことをすればするほど、コストは高くなっていく。等価性のチェックにかかるコストが再レンダーにかかるコストより高くなれば、意味がないどころか逆効果ということになる。そのため、常にReact.memoを使えばよいというものではない。冒頭に書いたように試行と計測を繰り返して、最適化を行っていく。パフォーマンス向上に僅かな効果しか得られない場合、他の要素(可読性やメンテナンス性など)とのトレードオフを考慮し、敢えて最適化しないという選択肢も十分にあり得る。組み合わせるメモ化が使われている機能はReact.memoだけではない。useCallbackもそのひとつ。useCallbackはメモ化を利用して、関数の不要な再作成を防ぐ。ある関数コンポーネントのなかに以下の記述があったとする。props.b);};showAandBは、再レンダーされる度に新しく作られる。props.aとprops.bが変わらない限り、showAandBを新しく作り直す必要はない。useCallbackを使えば、以前作ったshowAandBを再利用できる。props.b]);useCallbackの第一引数に作成する関数を、第二引数にその関数が使用している値を列挙した配列を、渡す。すると、配列に列挙した値を前回と比較し、それがひとつでも変われば、新しくshowAandBを作り直す。全て前回と同じであれば、前回のshowAandBを再利用する。すると、前回と同じ参照の関数を得ることになる。関数やオブジェクトの等価性は参照が同じかどうかで判定することが多いので、このことはメモ化を利用する上で大きな意味を持つ。サンプルを少し書き換えて、ChildにvalueとonClickを渡すようにした。valueは数値だが、onClickは関数である。appを押下したときも再レンダーされてしまう。原因はprops.onClickにある。compareすると、valueは等価であると判断される。onClickとして渡されているalertChildStateは、Appが再レンダーされる度に新しく作られるため、参照が異なり、常に等価性がないと判断されてしまう。このようなケースで、useCallbackが役に立つ。前述のように同じ参照の関数を返すので、React.memoが等価性があると判断するようになる。alertChildStateの定義を以下のように書き換えると、state.childが変わったときにのみ関数を作り直すようになる。childが押下された時にのみ、Childが再レンダーされるようになった。よるメモ化の影響範囲React.memoで再レンダーをスキップすると、そのコンポーネントの子コンポーネントの再レンダーもスキップされる。React.memoを使わない場合を見てみる。Appが持っているstateを、Child、そしてGrandchildと受け渡している。App;当然、ボタンを押す度に全てのコンポーネントが再レンダーされる。React.memoを使ってChildの再レンダーを常にスキップするようにすると、ChildだけでなくGrandchildの再レンダーも行われなくなることを確認できる。返す書き方は通常はしない);さらに検証してみると、GrandchildをReact.memoでラップしても、そのReact.memoは実行されていない。});Childの場合はareEqualを実行した上で再レンダーするかどうかを決めているが、Grandchildの場合はareEqualの実行自体が行われていない。このことから、React.memoで再レンダーをスキップすると、その時点でその下のコンポーネントツリーの処理が全てスキップされるのだと思われる。2019の6日目の記事です。2019の5日目の記事です。バージョンは16.8.4。はてなブログPro30歳無職から独学でプログラミングを開始した人間の記録。月別アーカイブ@numb_86さんのツイートスポンサーリンクはてなブログをはじめよう!numb_86さんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。