💧

【React】Hydrate時のワーニングを放置するとパフォーマンスが悪くなる!?

2021/05/27に公開

追記 (2022/04/19)

React18より、この記事で解説している内容はワーニングからエラーへ引き上げられました。

https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#other-breaking-changes

この記事で解説しているのは、React17の頃にワーニングレベルであったこの警告を「なぜ放置してはいけないか」ということについてです。
残念ながら 「どうやって解消すべきか」については触れていません ので、問題解決を急ぎたい方は、今すぐこの記事を閉じて他の記事を探してください。

Warning: Prop `XXX` did not match.

Next.js で開発をしていると、このようなワーニングにたまに遭遇する。

Warning: Prop `XXX` did not match. Server: yyyyyy Client: zzzzzz 

このワーニングは無視してはいけない。パフォーマンスの低下に直結する。

こちらは、とあるサービスのページにおける、微妙なコンテンツ差異の解消前後での、lighthouseスコアの比較。コンテンツ量が多いページで比較すると、このように差が顕著に出る。

解消前 解消後
1
2

ちなみに、Chromeのパフォーマンスプロファイルで見ると、「before-hydrate」 + 「hydrating」 に100~300msの差が出ていた

このワーニングが発生しているときは、実は CSR が発生している。たとえ SSG/SSR していてもだ。

何が起こっているのか

ReactDOMServer と Hydrate

React アプリケーションで CSR する時、 ReactDOM.render を使って DOM をマウントしている。
React でアプリケーションを構築した人は、大半の人が見たことがあるだろう。

ReactDOM.render(App, document.getElementById('root'))

しかし、Next.js や Gatsby などの、 SSR/SSG を行うフローではこのマウントの方法が少し異なる。

引用: How to implement server-side rendering (SSR) in your React application with NodeJS – step by step tutorial

https://lebersoftware.hu/react-server-side-rendering-step-by-step-tutorial/

上の画像の2番では、ReactDOMServer によって、React のコードから HTML が生成される。
https://ja.reactjs.org/docs/react-dom-server.html

その後、HTML と JS がクライアントに送信され、5番が処理されるが、実行されているのは ReactDOM.render ではなく ReactDOM.hydrate である。
https://ja.reactjs.org/docs/react-dom.html#hydrate

ReactDOM.hydrate(App, document.getElementById('root'))

ReactDOM.hydrate は、 サーバサイドで生成された DOM構造と、クライアントサイドで生成された仮想DOMが一致していることを期待している。
一致していれば、DOMのレンダリングをスキップし、イベントリスナーの登録だけを行う。

しかし、もし何らかの原因で DOM が一致しなかった場合、React は(可能な限り) DOM の状態を再現するために再レンダリングを行う。

React はレンダーされる内容が、サーバ・クライアント間で同一であることを期待します。React はテキストコンテンツの差異を修復することは可能ですが、その不一致はバグとして扱い、修正すべきです。開発用モードでは、React は両者のレンダーの不一致について警告します。不一致がある場合に属性の差異が修復されるという保証はありません。これはパフォーマンス上の理由から重要です。なぜなら、ほとんどのアプリケーションにおいて不一致が発生するということは稀であり、全てのマークアップを検証することは許容不可能なほど高コストになるためです。

つまり

サーバサイドで生成しテキスト化したDOMと、クライアントサイドで計算したDOMとの間に差分が生じると、ページ全体が再レンダリングされることになる。
特定のコンポネント一箇所だけで起こっていたとしても、ReactDOM.hydrate はルートエレメントに対して行われるため、対象コンポネントだけ再レンダリングというわけにはいかない。
この処理がかなり高コストであり、非力なモバイル端末だとパフォーマンスに影響がでる。

対処

対処方法に関しては、下の記事を参考にされたし。
https://zenn.dev/takewell/articles/5ee9530eedbeb82e4de7

先に述べた通り、useEffect は Hydrate 後に行われるため、useEffectuseState を組合せ、マウントされているかどうかを管理して分岐すると、問題は最小限に抑えられる。
「最小限に抑えられる」というのは、あくまでレンダリングを遅延させただけで、そのコンポネントに関しては SSG/SSR の恩恵を受けられなくなるため。

GitHubで編集を提案

Discussion