Reactのメモ化を使用することで、ウェブアプリケーションを小さなコンポーネントに分割し、再利用しやすくすることができます。
コンポーネントの更新が必要な場合、Reactは再レンダリングを契機に、動的なデータやアニメーションなどを表示する方法です。
しかし、再レンダリングが必要ないコンポーネントを再レンダリングするとなると、アプリケーションのパフォーマンスに悪い影響を与えます。
以下の状況を想像してみてください。
親コンポーネントが、コールバック関数を子コンポーネントにpropsを通じて渡す場面です。
そして、子コンポーネントはメモ化されているにも関わらず、親コンポーネントが再レンダリングされるたびに子コンポーネントを再レンダリングしてしまう。
この問題を調査し、その修正方法を学んでいきましょう。
問題点
親コンポーネントは、コールバック関数を子コンポーネントにpropsを介して渡します。
子コンポーネントはメモ化されていますが、Reactは親コンポーネントが再レンダリングされるたびにそれを再レンダリングします。
何かが原因で、メモ化の特性を失わせています。
以下は親コンポーネントと子コンポーネントのコードスニペットです。
このコードを試したい場合は、こちらのCodeSandboxリンクをご覧ください。
_Numberコンポーネントには、再レンダリングごとに発生する重い操作が含まれています。
これが問題であることを特定するために、私たちはReact SDKに付属するSentryのwithProfilerメソッドを使用して、関心のあるすべてのコンポーネントをラップします。
これにより、その特定のコンポーネントのui.react.mountおよびui.react.updateイベントがキャプチャされます。アプリをリロードし、「増加」ボタンを数回クリックすると、Sentryパフォーマンスダッシュボードで以下のように表示されます。
トランザクションのうち50%がUI操作に費やされたことがわかります。
こちらについて詳細に調べる必要がありそうです。
しかし、なぜでしょうか?
私たちは_NumberコンポーネントをReactのmemo()でラップしました。
なぜそれが再レンダリングされ続けるのでしょうか?
Reactと再レンダリングに関して知っていることを考えると、Reactはコンポーネントを再レンダリングするとき、それらの状態またはプロパティのいずれかが変更されたときです。
_Numberコンポーネントを見てみると、状態変数が定義されていないことがわかりますが、propsからsetMessageコールバックを受け入れています。
問題は「増加」ボタンをクリックしたときに発生します。
_Numberコンポーネントには全く関係ありませんが、それによりClosureRerenderコンポーネントが再レンダリングされ、それが_Numberコンポーネントに渡されるonClickメソッドを再作成します。
_Numberコンポーネントはメモ化されていますが、親が再レンダリングされるたびにsetMessageプロパティに異なる値を受け取り、これによりメモ化をバイパスし、再レンダリングします。
onClickメソッドは変更されないにもかかわらず、その参照が変わります。
自分で確認したい場合は、このページでコンソールを開いて、次のように一行ずつ入力してみてください。
最後のx===yコマンドはfalseを出力します。
それにもかかわらず、両方のオブジェクトは同じ値(’Lazar’)を持つ同じ名前のプロパティを持っています。
JavaScriptは、非プリミティブ型を扱う際に変数の値として参照を保持し、手動で両方のオブジェクトを作成したため、xとyは異なる参照を持ち、したがってx===yはfalseになります。
Reactでも同様です。ClosureRerenderが再レンダリングされるとき、onClickメソッドが再作成されるため、実質的に新しい参照が渡されます。
古いsetMessageプロパティは新しいものの値と一致しないため、Reactは_Numberコンポーネントを再レンダリングします。
では、これを修正するにはどうすればよいのでしょうか?
解決策
解決策としては、`useCallback`フックを使用する必要があります。
`useCallback`フックは、コールバックに対する`useMemo`や`memo()`がコンポーネントに対するものであるようなものです。
依存関係の配列に変更がない限り、コールバックの再作成を防ぎます。
新しい`onClick`メソッドは次のように記述します。
このメソッドを`useCallback`フックでラップし、`props.setMessage`を依存関係の配列に配置します。
それが変更されない限り、再レンダリングの間に`onClick`は同じ参照値を保持し続けます。
もはや「増加」ボタンをクリックしても_Numberコンポーネントの再レンダリングがトリガーされません。そして、それをSentryで検証できます。
ずっと良くなりました。
不必要なui.react.updateイベントもなく、長時間実行されるUIブロッキングタスクもありません。
結論
`useMemo`フックや`memo()`メソッドは、常にコンポーネントが不必要に再レンダリングされるのを防ぎません。
今回の記事のようにメモ化を壊してしまう場面があり、それによってパフォーマンスが損なわれることがあります。
これは、`useCallback`フックを使用しないコールバックメソッドを渡す場合だけでなく、`setMessage={(number) => props.setMessage(number)}`のようにコールバックをインラインで定義する場合にも発生する可能性があります。
慣習的にコードを書いていると思うので「それが何を引き起こすか」にまでは、あまり注意を払っていないかもしれません。
今回のように、これらの状況をアプリ全体で修正したことを検証し、Reactアプリのパフォーマンスを監視し始めるために、アプリにSentryを導入してみてください。
開始は無料で、インストールも簡単です。
SentryがReactアプリに対して何ができるかをもっと詳しく知りたい場合は、Sentry for React ページをご覧ください。
現在、Reactで不必要な再レンダリングに特有のパフォーマンスの問題を作成すべきかどうかのフィードバックを収集しており、あなたの意見を聞かせていただきたいと思っています。
これに関する特定のパフォーマンスの問題を持つことは、すべてのコンポーネントの再レンダリングの発生を追跡できるようにし、アプリ内でそれが発生したときに自動的にJIRAのタスクを作成したり、Slackやメールのアラートを作成したりすることができるようになります。
このGitHubのイシューにアクセスし、ディスカッションに是非ご参加ください。
コンポーネントの再レンダリングのパフォーマンス問題を望む声が多ければ多いほど、その機能が優先的にアップデートされることでしょう。
是非、あなたの声をお聞かせください。
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。