【React】Hydrate時のワーニングを放置するとパフォーマンスが悪くなる!?
追記 (2022/04/19)
React18より、この記事で解説している内容はワーニングからエラーへ引き上げられました。
この記事で解説しているのは、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 を行うフローではこのマウントの方法が少し異なる。
上の画像の2番では、ReactDOMServer
によって、React のコードから HTML が生成される。
その後、HTML と JS がクライアントに送信され、5番が処理されるが、実行されているのは ReactDOM.render
ではなく ReactDOM.hydrate
である。
ReactDOM.hydrate(App, document.getElementById('root'))
ReactDOM.hydrate
は、 サーバサイドで生成された DOM構造と、クライアントサイドで生成された仮想DOMが一致していることを期待している。
一致していれば、DOMのレンダリングをスキップし、イベントリスナーの登録だけを行う。
しかし、もし何らかの原因で DOM が一致しなかった場合、React は(可能な限り) DOM の状態を再現するために再レンダリングを行う。
React はレンダーされる内容が、サーバ・クライアント間で同一であることを期待します。React はテキストコンテンツの差異を修復することは可能ですが、その不一致はバグとして扱い、修正すべきです。開発用モードでは、React は両者のレンダーの不一致について警告します。不一致がある場合に属性の差異が修復されるという保証はありません。これはパフォーマンス上の理由から重要です。なぜなら、ほとんどのアプリケーションにおいて不一致が発生するということは稀であり、全てのマークアップを検証することは許容不可能なほど高コストになるためです。
つまり
サーバサイドで生成しテキスト化したDOMと、クライアントサイドで計算したDOMとの間に差分が生じると、ページ全体が再レンダリングされることになる。
特定のコンポネント一箇所だけで起こっていたとしても、ReactDOM.hydrate
はルートエレメントに対して行われるため、対象コンポネントだけ再レンダリングというわけにはいかない。
この処理がかなり高コストであり、非力なモバイル端末だとパフォーマンスに影響がでる。
対処
対処方法に関しては、下の記事を参考にされたし。
先に述べた通り、useEffect
は Hydrate 後に行われるため、useEffect
と useState
を組合せ、マウントされているかどうかを管理して分岐すると、問題は最小限に抑えられる。
「最小限に抑えられる」というのは、あくまでレンダリングを遅延させただけで、そのコンポネントに関しては SSG/SSR の恩恵を受けられなくなるため。
Discussion