11
4

More than 1 year has passed since last update.

【Next.js】Warning: X did not match.Server: y Client: z は絶対に解消しろ

Last updated at Posted at 2021-05-27

追記 (2022/04/19)

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

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

結論

パフォーマンスに与える影響が非常に大きい。
「コンソールにワーニング出てるけど、レンダリングされる結果は問題ないからいいか」ぐらいの軽い気持ちで無視すると痛い目を見る

Warning解消前 Warning解消後
1回目 mismatch.png match.png
2回目 スクリーンショット 2021-05-27 10.48.48.png スクリーンショット 2021-05-27 10.49.57.png
ちなみに、このサンプルはJSON-LD(構造化マークアップデータ)の出力がサーバサイドとクライアントサイドに微妙な差異が生じていたときの例。コンテンツ量が多めのページで比較すると、差が顕著に出る。

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

SSG/SSR しているのに、CSRしているのと何も変わらない状態になる。

何が起こっているのか

ReactDOMServer と Hydrate

CSR な React アプリケーションは 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

上の画像の2番では、ReactDOMServer によって、React のコードから HTML が生成される。
https://ja.reactjs.org/docs/react-dom-server.html
(このときの HTML は、useEffect のような、マウントに対しての副作用的な処理が実行される前の 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 は、 サーバサイドで生成された HTMLのDOM構造と、クライアントサイドで生成された仮想DOMが一致していることを期待している。
一致していれば、DOMの再レンダリングをスキップし、イベントリスナーの登録だけを行う。
もし一致しなければ、(可能な限り)DOMの状態を再現するために再レンダリングを行う。

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

つまり

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

対処

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

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

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4