メインコンテンツへスキップ

Next.js hydration mismatch 対処法の決定木

· loading · loading ·
kiitosu
著者
kiitosu
aws community builder. 画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

はじめに
#

Next.jsでSSRやSSGを使っていると、誰しも一度は踏む Hydration failed because ... エラー。対処法は複数ある一方、SNSや解説記事は個別解法の紹介が中心で、「どういうときにどれを使うべきか」の全体像が見えにくいのが実情です。

本記事では、hydration mismatchの原因を5分類し、解決手段を5つ整理した上で、選び方を決定木にまとめた辞書型記事を目指します。迷ったときの早見表として使ってもらえれば幸いです。

📝 用語:本記事で「レンダリング」とは

Reactのコンポーネント関数を実行して HTML(または仮想DOM)を出力する処理 を指します。ブラウザがピクセルを画面に描画する工程(Paint)とは区別します。SSRならサーバーが、SSGならビルドマシンが、hydration時はブラウザがそれぞれ「レンダリング」を行います。

なぜ「hydration」と呼ぶのか
#

hydrationという単語、語源的には「水を与える」という意味です。

語源
#

ギリシャ語 ὕδωρ (hydor) = 「」が根っこ。そこから派生した英単語:

英単語 意味 関連
hydration 水分を与える/水和 本題
hydrogen 水素(=水のもと) 化学
dehydration 脱水 医療・食品
hydra 水蛇(多頭の怪物) ギリシャ神話
hydrant 消火栓 都市

全て同じ「水」の語根でつながっています。

日常での「hydration」
#

  • Stay hydrated!」=「水分補給してね!」
  • 化学の「水和反応」= 物質に水分子を結合させる
  • スキンケアの「hydrating cream」= 保湿クリーム

いずれも「乾いているものに水を与えて本来の姿にする」イメージです。

ソフトウェア用語としてのhydration
#

これは比喩です。

乾燥食品(骨だけ)   + 水  =  戻った食品(食べられる)
SSRで作った静的HTML  + JS  =  動くReactアプリ
  • サーバーが返すHTMLは「骨だけのビーフジャーキー」状態(見た目はある、でも動かない)
  • ブラウザが JSバンドルを読み込んで各要素に挙動を紐付ける=「水を注いで元通り」
  • ReactのイベントハンドラやuseStateの魂がDOMに宿る

語源としての「的確さ」
#

この比喩がソフトウェアで定着した理由:

  • 見た目は同じまま中身が生き返る(DOM構造は変わらず、機能だけ追加)
  • dry/wetの対比に馴染みがある(プログラミングではDRY原則も定着しているし、dry run・wet runの語感もある)
  • 「乾燥 → 水分補給 → 生きる」という段階的な復活のニュアンス

「ただJSを後から実行する」と言うより、「乾いた骨に命を吹き込む」と言うほうが感覚的。Reactコミュニティが hydrate() という関数名を選んで広まった経緯もあり、今では一般用語化しています。

そもそもhydration mismatchとは何か
#

サーバーレンダリングとhydrationの流れ
#

まず、HTMLは事前にどこかで作られます(hydrationとは別のフェーズ)。

  • SSR(Server-Side Rendering):リクエスト時にサーバー(Node等)が生成
  • SSG(Static Site Generation):ビルド時にビルドマシンが生成 → CDN配信

ブラウザに届いたあとの流れは、SSR・SSGどちらも共通です。

  1. HTML受信・表示:受け取ったHTMLをブラウザが表示(Paint)。JSはまだ動いていない
  2. JSバンドル読込
  3. hydration:ブラウザのReactがコンポーネントをレンダリングして仮想DOMを作り、既存のDOMと突き合わせてイベントハンドラや状態を紐付ける(既存DOMの再生成はしない)
flowchart LR
  A["SSR: サーバーで生成
SSG: ビルド時に生成"] --> B["Browser
HTML受信・表示"] B --> C[JS読込] C --> D["hydration
イベントハンドラ紐付け
React内部状態構築"]
SSR/SSGの違いと個別化の選択肢(詳細)

SSR/SSGの違いは前半フェーズの「いつ誰が作るか」だけです。hydrationの仕組みはどちらでも同じなので、本記事の議論も両方に当てはまります。

SSR SSG
HTML生成タイミング リクエスト時 ビルド時
生成場所 サーバー(Node等) ビルドマシン → CDN配信
動的ユーザーデータ リクエストごとに変えられる(ログインユーザー名、カート内容等) 全ユーザー共通のHTMLが返る(個別データはクライアント側で後から取得するか、middleware等で差し込む)

SSGで「ユーザーごとに違う表示」が必要な場合の選択肢:

  • ビルド時にはプレースホルダーを出し、hydration後にクライアントからAPIを叩いて差分を埋める
  • Next.js の Middleware を使い、Cookie/Headerから読んでサーバー側で差分を注入する

個別化が大きすぎてSSGで捌ききれないなら、そのページは SSRに切り替える 判断も必要です。SSGは速い・安いが個別化しにくい、SSRは個別化できるがサーバーコストとレイテンシが増える、というトレードオフがあります。

※ Next.js App Router(RSC)はこの単純モデルより複雑ですが、Client Componentsのhydrationはここでのモデルとほぼ同じです。

mismatchとは
#

hydrationの正体は、「サーバー生成HTML」と「ブラウザでReactが同じコンポーネントを再度レンダリングした結果」を突き合わせる作業です。両者がタグ・属性・テキストのどれかで食い違っていると mismatch になります。

ブラウザのReactは盲目的に再描画しているわけではなく、サーバーと同じ入力(コンポーネントのコード+props) で実行する前提で動きます。入力となるデータ(props・初期ステート)はHTMLに埋め込まれた JSON(Pages Routerなら __NEXT_DATA__、App RouterならRSC Payload)として届きます。

flowchart TD
  SC["コンポーネント関数 + props"]
  SC --> SR_SSR["SSR
リクエスト時に
サーバーがレンダリング"] SC --> SR_SSG["SSG
ビルド時に
ビルドマシンがレンダリング"] SC --> JS["JSバンドル
(bundlerがブラウザ向けに
コンポーネント関数を変換)"] SR_SSR -->|"即時配信"| H["HTML + 初期データJSON"] SR_SSG -->|"CDN経由で後日配信"| H H --> B["ブラウザ到達"] JS --> B B --> D[既存DOM] B --> R["ブラウザのReact
同じpropsでコンポーネント実行"] R --> V[virtual DOM] D --> C{比較} V --> C C -->|一致| OK[hydration成功] C -->|不一致| NG[mismatch]

入力(コード+props)が同じでも、コンポーネント内で入力以外のものnew Date() / Math.random() / window / localStorage 等)を読むと、ブラウザでの再実行結果がサーバーから送られてきたDOMと一致しなくなります。これがmismatchの本質です。

SSR/SSGの違いは図の上部だけ。ブラウザ側は完全に同じです。ただしSSGはビルド時〜実行時の時間差が大きくなり得るため、new Date() のような時刻依存値はSSGでより顕著にmismatchを起こします(逆にビルド時点で確定する値しか使わないと割り切れば、SSGではmismatchを原理的に避けやすくなります)。

不一致の3パターン
#

① テキストの不一致

function Now() {
  return <p>{new Date().toISOString()}</p>;
}
// サーバー:  <p>2026-04-19T10:00:00.000Z</p>
// ブラウザ:  <p>2026-04-19T10:00:03.512Z</p>
// → テキストが違う

② 属性の不一致

function Card() {
  const theme = typeof window !== 'undefined'
    && localStorage.getItem('theme');
  return <div className={theme === 'dark' ? 'dark' : 'light'}>Hi</div>;
}
// サーバー: <div class="light">Hi</div>  (localStorageが無い扱い)
// ブラウザ: <div class="dark">Hi</div>   (localStorageに dark がある)
// → class属性が違う

③ 構造の不一致

function Banner() {
  const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
  return isMobile ? <span>モバイル</span> : <div>デスクトップ</div>;
}
// サーバー: <div>デスクトップ</div>
// ブラウザ: <span>モバイル</span>
// → タグ自体が違う

何が問題か
#

  • Reactはmismatchを検知すると warning を出す(dev環境のみ表示)
  • React 18以降:mismatchした部分をクライアント側で 再描画してリカバリ するが、チラつき・レイアウトシフトが起きる
  • 内容によっては まったく別のDOMが出る(条件分岐で片方だけ描画等)→ 初期表示が壊れる
  • パフォーマンスの無駄:サーバーで生成したHTMLが置き換えられる=サーバー側の描画コストが無効化される
  • Core Web Vitals悪化:再描画で発生するCLS(レイアウトシフト)はGoogleの評価指標に影響し、間接的にSEOにも響く

原因パターン5分類
#

# パターン 具体例
1 ブラウザ専用API window, localStorage, navigator
2 時刻・ランダム値 new Date(), Math.random(), crypto.randomUUID()
3 ユーザー固有表示 OS由来のテーマ、ロケール、タイムゾーン
4 サードパーティDOM注入 Google翻訳、ブラウザ拡張、広告タグ
5 環境差異 User-Agent、画面幅、Cookie有無

解決手段カタログ
#

後述の決定木と同じ順序で並べます。上から順に「まず検討すべき」手段です。

手段 用途 トレードオフ
レンダリング時に確定 Cookie/Header から読む / ビルド時に埋め込む(mismatchを発生させない最良手) 取得経路の見直し/middleware・header関数が必要
dynamic(..., {ssr:false}) このコンポーネントだけ事前レンダリングから外す 初期表示が遅れる
useEffect + mountedフラグ ブラウザ専用値の一部だけを差し替える SSR時のコンテンツが空(or 既定値)になる
suppressHydrationWarning 時刻等の意図的なmismatch 本来検知すべき別のmismatchも消えるリスク
要件レベルで見直し 他4手段がハマらないときに前提や構造を疑う(CC分割 / 要件再検討) 設計変更が必要、影響範囲が広い

決定木(この記事のキモ)
#

まず疑うこと:この値は本当にクライアント固有か?
#

hydration mismatchに当たったら、解決手段を探す前に一歩立ち止まりましょう。多くのケースで「本当はレンダリング時に確定できる値を、ブラウザ側で取ってしまっている」だけだったりします。

  • localStorage に保存していたテーマ設定 → Cookieに移せばレンダリング時に読める
  • navigator.language で判定していた言語 → Accept-Language ヘッダで読める
  • CSR時代のコードをそのままSSR化した結果 → 値の取得経路を見直す

レンダリング時に確定できるなら、それがmismatchを発生させない最良の手段です。決定木はこの問いを最初に置きます。

決定木
#

flowchart TD
  Q0["この値は本当にクライアント固有か?
(Cookie/Headerに移せないか?)"] Q0 -->|"NO(レンダリング時に確定可能)"| A0["レンダリング時に確定
(Cookie/Headerから読む
or ビルド時に埋め込む)
= mismatchを発生させない最良手"] Q0 -->|"YES(真にクライアント固有)"| Q1{このコンポーネントだけ
事前レンダリングから外してよい?} Q1 -->|YES| A1["dynamic(..., ssr:false)"] Q1 -->|NO| Q2{サーバー空表示を許容できる?} Q2 -->|YES| A2["useEffect + mountedフラグ"] Q2 -->|NO| Q3{mismatchを意図的に許容?
タイムスタンプ等} Q3 -->|YES| A3["suppressHydrationWarning
+ 理由コメント"] Q3 -->|NO| A4["要件・設計を見直し"]

最後に
#

hydration mismatchは「Reactがサーバー/ビルド時とブラウザで、同じコンポーネントをレンダリングした結果が食い違う」現象です。根本原因は 入力(コード+props)以外の値 をレンダリング中に読んでしまうこと、この1点に尽きます。

よくある罠

  • suppressHydrationWarning の濫用:本来検知すべき別のmismatchまで握りつぶします。局所的に、かつ理由コメント付きで使いましょう
  • typeof window !== 'undefined' ガードで済ませる:サーバーのクラッシュは回避できますが、サーバー/ブラウザで出力が分岐するのでmismatchを自ら作り出している状態です。決定木の手段に置き換えましょう
  • localStorage と Cookieの取り違え:SSRで読みたい値はCookieへ。localStorage はブラウザ専用データに限定します
  • 1つの大きなCCに複数の問題を抱える:各問題に別々の手段を当てるため、コンポーネント単位に決定木を走らせる のがコツです

hydration mismatch に出くわしたら決定木を参考にしてみて下さい!

Reply by Email

関連記事

CloudWatch Pipelinesを使ってみた
いまさら AWS AppConfig に触ってみた
· loading · loading
Lambda Durable Functionsを使ってみる — Step Functionsとの比較からハンズオンまで
· loading · loading