Next.jsのSSRで「ローカルでは動くのに本番だけHydration mismatchが出る」という状況、めっちゃ消耗しますよね。この記事は、そんな詰まり方をしている人に向けた実践ガイドです。読むと、勘に頼らず10分で原因候補を絞る手順が作れます。僕は福祉事業のIT全般を担当しつつ、AI×SaaS開発でも障害調査を回しているので、現場でそのまま使える形に落として共有します。
こんな方におすすめ
- Next.jsでHydration mismatchが散発していて原因が追えない方
- Server Functionsのログを見ても、どこまで信用していいか迷う方
- 本番障害の初動を「担当者の経験値依存」から抜けたい方
- 非エンジニアメンバーにも説明できる調査手順がほしい方
この記事でわかること
- Next.js 16.2で追加された観測ポイントの使い分け
- 0〜10分で回すHydration mismatch切り分けRunbook
- 原因別の最短対処(Date/window/不正HTML/SSR非対応)
- 本番監視と再発防止まで含めた運用テンプレート
公開前確認(2026年5月時点):Next.js 16.2系のログ調査では、ブラウザログ転送・Server Functionログ・Vercel Logsのrequest-idを同じ時系列で見ると、Hydration mismatchの再現条件を短時間で絞れます。
Next.js 16.2でデバッグが変わったポイント
Next.js 16.2(2026-03-18公開)で、Hydration調査の初動はかなり変わりました。今までは「ブラウザ警告」「サーバーログ」「実際の差分」を別々に見ていたので、原因特定まで30分以上かかることが多かったです。16.2ではServer Functionsの実行ログとHydration差分表示が揃ったので、1回の再現で根拠を集めやすくなりました。
いちばん大きい変化は、+ Client / - Serverの差分がその場で見えることです。イメージとしては、Git diffをブラウザ上で見ながらSSR不整合を潰す感覚です。加えて、next dev起動はハイライトで約400%高速、実アプリでもHTMLレンダリングが25〜60%高速という改善が出ていて、再現テストの回転も上げやすいです。
| 観点 | 16.1まで | 16.2以降 |
|---|---|---|
| Server Functions追跡 | 独自ログ実装が必要 | logging.serverFunctionsで即確認 |
| Hydration差分 | 警告文から推測 | + Client / - Serverで可視化 |
| ブラウザ警告共有 | 画面を見ないと気づきにくい | browserToTerminalでターミナル集約 |
| 調査初動 | 人によってばらつく | 手順化しやすい |
注意点も1つあります。browserToTerminalのデフォルト記載は、16.2 AI記事とconfig docsで差があります。自分の16.2.x環境で実挙動を先に確認しておくと、ログノイズで迷いません。公式はNext.js 16.2リリースとlogging設定をセットで見るのがおすすめです。
10分で切り分けるRunbookの全体像
僕が現場で回している流れは、0〜2分で事象固定、3〜6分で差分特定、7〜10分で原因確定です。ここを固定すると、担当者が変わっても調査品質が落ちません。つまり「うまい人だけ早い」状態を抜けられます。
- 0〜2分: 事象固定 — URL、操作手順、発生タイミングを1行で記録します。
next devが二重起動しているとログが混ざるので、`.next/dev/lock`とPID表示でまず整理します。 - 3〜6分: 差分特定 — Hydration Diff Indicatorで
+ Client / - Serverを見て、テキスト差分かDOM構造差分かを分けます。ここで原因候補を2〜3個に絞ります。 - 7〜10分: 原因確定 —
logging.serverFunctionsとbrowserToTerminalを同時に見て、関数実行の前後関係を確認します。Date由来か、`window`参照か、SSR非対応ライブラリかを決めて修正方針を選びます。
初動を速くするコツ
- 再現条件を固定 — ブラウザ拡張を切り、時刻依存画面は同じタイムゾーンで確認します。
- ログ粒度を固定 —
browserToTerminalはまずwarnで開始し、必要ならtrueに上げます。 - 記録形式を固定 — 「現象/差分/原因/修正」を4行で残すだけで、次回が早くなります。
この10分フローは、非IT部門のメンバーへ説明するときも強いです。工程が時系列なので、技術用語が多くても「今どこを見ているか」を共有しやすいです。
logging.serverFunctionsとbrowserToTerminalの最短セットアップ
まずは観測点を揃えます。ここを雑にすると、直したつもりで再発します。next.config.jsは次の形から始めるのが実務では安定しました。
/** @type {import('next').NextConfig} */
const nextConfig = {
logging: {
serverFunctions: true,
browserToTerminal: 'warn',
},
}
module.exports = nextConfig
この設定で、Server Functionの関数名・引数・実行時間・定義ファイルがターミナルに出ます。警告以上のブラウザログも同じ場所に集まるので、視線移動が減って判断が速くなります。ログが多すぎる期間だけserverFunctions: falseに落とす運用もありです。
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const user = await requireUser()
if (!user) throw new Error('Unauthorized')
await db.post.create({
data: { title: String(formData.get('title') ?? '') },
})
revalidatePath('/posts')
}
- 確認1 — 関数が呼ばれた時刻とHydration警告時刻が一致しているか
- 確認2 — ログ上の引数と、画面上の表示データが一致しているか
- 確認3 — 認証失敗やnull値が「復元エラー」扱いで隠れていないか
運用メモとして、外部導線ではなく、実測ログ・設定差分・再現手順を同じ場所に残してチーム内で確認できる形にしてください。
Hydration mismatch原因別トリアージ(最短版)
公式は主因を7分類で整理していますが、実務ではまず5分類に寄せると速いです。最初から完璧に特定しようとせず、再現性の高い順に潰すのがポイントです。
| 原因パターン | 典型症状 | 最初に見る場所 | 最短対応 |
|---|---|---|---|
| Date/Random | 時刻やIDが毎回ズレる | 初回描画のテキスト | クライアントで再計算 |
| window/localStorage | 本番のみ警告 | サーバー実行パス | useEffectへ移動 |
| 不正HTMLネスト | 差分位置が毎回変わる | 該当コンポーネントのJSX | ネスト修正 |
| SSR非対応ライブラリ | 特定ウィジェットだけ崩れる | import箇所 | dynamic(..., { ssr:false }) |
| CDN/拡張による改変 | 本番だけDOMが変わる | 配信後HTML | 配信設定と拡張影響確認 |
核心は「初回SSRと初回クライアント描画を同じに保つ」ことです。ここが揃えば、Hydration mismatchの大半は消えます。
'use client'
import { useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
export function ClientOnlyTime() {
const [time, setTime] = useState('')
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <p>{time || '読み込み中...'}</p>
}
export const NoSSRChart = dynamic(() => import('./Chart'), { ssr: false })
suppressHydrationWarningは最後の逃げ道として使うのが安全です。根本原因を残したままにすると、次の改修で別の場所にズレが出やすいです。
本番障害向けの監視設計と運用テンプレート
ここからは「直して終わり」にしない話です。ReactのhydrateRootはonRecoverableErrorなどのフックを持っているので、復元可能エラーも監視に送れます。これを入れるだけで、見逃していた小さな不整合が可視化されます。
import { hydrateRoot } from 'react-dom/client'
import App from './App'
import { reportError } from './lib/reportError'
const root = document.getElementById('root')
if (root) {
hydrateRoot(root, <App />, {
onRecoverableError: (error, info) => reportError('recoverable', error, info.componentStack),
onCaughtError: (error, info) => reportError('caught', error, info.componentStack),
onUncaughtError: (error, info) => reportError('uncaught', error, info.componentStack),
})
}
- 監視 — Recoverable/Caught/Uncaughtを別イベントで保存します。
- 認証 — Server Functionsは
POSTで外部到達の可能性があるので、各関数で認可チェックを必ず入れます。 - 再発防止 — 修正後24時間で同種エラー件数が減ったかを確認します。
## Hydraion Incident Log
- 発生日:
- URL:
- 再現手順:
- Diff (`+ Client / - Server):
- 原因分類:
- 修正内容:
- 再発防止:
運用メモとして、外部導線ではなく、実測ログ・設定差分・再現手順を同じ場所に残してチーム内で確認できる形にしてください。
Hydration mismatchを放置した先に待つ現実
この手順を知らないまま運用すると、重大障害だけでなく「小さな違和感」が積み上がります。たとえば初回表示のチラつきや、フォーム送信の取りこぼしです。すぐに致命傷には見えなくても、1年単位では信頼低下につながる可能性があります。
- 調査時間の増大 — 毎回ゼロから調べるので、障害1件あたりの工数が膨らきやすいです。
- 説明コストの増大 — 非エンジニア向け説明が属人化し、意思決定が遅れやすいです。
- 再発率の上昇 — 根本原因が残り、別画面で同系統の不整合が再登場しやすいです。
この記事を書いている理由
僕自身、2025年10月に結婚式Web招待状サービスを自作して即リリースし、90日以上ログイン継続率を維持しながら改善を回してきました。そのとき痛感したのが、障害対応は「気合い」より「手順」のほうが強いということです。今は福祉事業のIT全般を担当していて、非ITの現場言語で説明しながら復旧する場面が多いです。だからこそ、ITの武器を非IT領域に持ち込む異世界転生の視点で、再現しやすいRunbookとして共有したいと思っています。
次のアクション
運用メモとして、外部導線ではなく、実測ログ・設定差分・再現手順を同じ場所に残してチーム内で確認できる形にしてください。