lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

【React】ちゃんと知って使うReact.memo

この記事は Wano Group Advent Calendar 2022の10日目の記事となります。

はじめに

改めてですが、私は音楽ディストリビューションサービス「TuneCore Japan」のフロントエンドエンジニアを担当しております。そしてこの2022年も1年通してReactをゴリゴリ書き、コードレビューも何度もしてきました。TailWind CSSだけをひたすら書いてた時期もちょっとありましたが

そんな中、自分の中のもやもやとして上がっていたのがReact.memoによるパフォーマンス改善をやるべきコンポーネントとやるべきでないコンポーネントの明確な定義ができていないというところでした。この点にフォーカスして今回は React.memoをちゃんと知り、正しく使えるようにするために自分が調べたこと を整理したいと思います。

React.memoに関する公式での説明

以下、Facebookが提供している公式Docから引用します。

React.memo は高階コンポーネントです。

もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。

React.memo は props の変更のみをチェックします。React.memo でラップしているあなたのコンポーネントがその実装内で useState、useReducer や useContext フックを使っている場合、state やコンテクストの変化に応じた再レンダーは発生します。

デフォルトでは props オブジェクト内の複雑なオブジェクトは浅い比較のみが行われます。比較を制御したい場合は 2 番目の引数でカスタム比較関数を指定できます。

これはパフォーマンス最適化のためだけの方法です。バグを引き起こす可能性があるため、レンダーを「抑止する」ために使用しないでください。

自分なりの言葉で上記の説明を補足すると

  • React.memoは、propsオブジェクト内の値に対して、 Object.isを通した値の比較を適用して、仮にtrutyだった場合はレンダーフェーズにおける仮想DOMの差異チェックをスキップする

  • React.memo でラップしているコンポーネント内部のstateの変更に対しては、従来通り再レンダーが走る

といった感じでしょうか。ここで注意したいことは、React.memoコンポーネントをラップした場合、追加で Object.is によるprops同士の浅い比較がレンダリングごとの計算処理として追加されていることになります。

なので、仮に各レンダーフェーズにおいて毎回Propsの変更が行われるコンポーネントがあった場合は、React.memoでラップしても毎回再レンダリングされた上、追加でObject.is によるprops同士の浅い比較が行われるため結局総合的な計算量は増え、パフォーマンスに悪影響を及ぼすことになります。このことからも、慎重に利用するべきライブラリであることは間違いありません。

ではどういった時にReact.memoを使えば良いのか

上記のような仮説を自分なりに持って調査をしている時に出会った記事が

Use React.memo() wisely

になります。記事の第二章「2. When to use React.memo」が今回の疑問の解決に役立ちました。

詳細は記事を参照していただきたいですが、基本的な方針は 同じPropsで再レンダリングされる頻度が高い場合に React.memo を適用する、というところです。

筆者が記事の中で紹介している コンポーネント MovieMovieViewsRealtime のケースが理解しやすいと思います。

MovieViewsRealtimeコンポーネントはリアルタイムに情報が変わる頻度が高い箇所とstaticな情報をもつ箇所(titlereleaseDate)にDOM要素が分かれており

staticな情報をもつ箇所をMovieコンポーネントとして切り出して React.memoでラップすることで無駄な再レンダリングを防ぐ、と言うアプローチです。

このように、親コンポーネントの再レンダリングにつられて不必要な再レンダリングが子コンポーネントで発生するようなケースは多くの現場でのReactを通したUI開発でも肌感でよく起きている印象なので、このような場面においてはReact.memoが有効だと思いました。

また、筆者が「3. When to avoid React.memo」において指摘しているように、React.memoが悪影響を及ぼすパターンも多くあります。個人的にありがちだと思ったのが、コールバックのメモ化を怠った結果、常にReact.memoによるObject.is の値がfalseを返し意図しない再レンダリングが毎回行われてしまっていたと言うケースです。

さいごに

Reactの文脈においては、レンダリングの回数が減ったことが、そのままパフォーマンス向上につながったと考えられる傾向にあると予想しますが、コンポーネントによっては、React.memoによるpropsの比較が悪影響を及ぼし結果としてパフォーマンスに問題を起こすことは、今回の調査によって注意しなければならない、と改めて思いました。

Reactの公式サイトにも書かれていますが、React.memoに上記のようなリスクがあることは明らかなので、やはりレンダリング頻度や計算時間などを確認していくことは間違いなく必要になってくると思います。自分もProfilingを運用上で適切に活用して、より高品質なUI開発ができるように気をつけて行きたいところです。