Next.js 16.2ログでHydration mismatchを10分特定

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の再現条件を短時間で絞れます。

僕は福祉事業のIT全般を担当していて、日々の運用改善と障害対応を並行しています。複業ではAI×SaaS開発にも入っているので、開発速度と運用安定性を同時に見る立場です。この記事はその実務で磨いた手順を、再現しやすい形でまとめています。

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分で原因確定です。ここを固定すると、担当者が変わっても調査品質が落ちません。つまり「うまい人だけ早い」状態を抜けられます。

  1. 0〜2分: 事象固定 — URL、操作手順、発生タイミングを1行で記録します。next devが二重起動しているとログが混ざるので、`.next/dev/lock`とPID表示でまず整理します。
  2. 3〜6分: 差分特定 — Hydration Diff Indicatorで+ Client / - Serverを見て、テキスト差分かDOM構造差分かを分けます。ここで原因候補を2〜3個に絞ります。
  3. 7〜10分: 原因確定logging.serverFunctionsbrowserToTerminalを同時に見て、関数実行の前後関係を確認します。Date由来か、`window`参照か、SSR非対応ライブラリかを決めて修正方針を選びます。

初動を速くするコツ

  • 再現条件を固定 — ブラウザ拡張を切り、時刻依存画面は同じタイムゾーンで確認します。
  • ログ粒度を固定browserToTerminalはまずwarnで開始し、必要ならtrueに上げます。
  • 記録形式を固定 — 「現象/差分/原因/修正」を4行で残すだけで、次回が早くなります。

この10分フローは、非IT部門のメンバーへ説明するときも強いです。工程が時系列なので、技術用語が多くても「今どこを見ているか」を共有しやすいです。

logging.serverFunctionsbrowserToTerminalの最短セットアップ

まずは観測点を揃えます。ここを雑にすると、直したつもりで再発します。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のhydrateRootonRecoverableErrorなどのフックを持っているので、復元可能エラーも監視に送れます。これを入れるだけで、見逃していた小さな不整合が可視化されます。

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時間で同種エラー件数が減ったかを確認します。
Sentryを使う場合、Hydration系の一部は可読メッセージではなくminified invariant(423/425)で入ることがあります。除外や集計ルールは、メッセージ文字列ではなくコード条件でも持っておくと運用しやすいです。
## Hydraion Incident Log
- 発生日:
- URL:
- 再現手順:
- Diff (`+ Client / - Server):
- 原因分類:
- 修正内容:
- 再発防止:

運用メモとして、外部導線ではなく、実測ログ・設定差分・再現手順を同じ場所に残してチーム内で確認できる形にしてください。

Hydration mismatchを放置した先に待つ現実

この手順を知らないまま運用すると、重大障害だけでなく「小さな違和感」が積み上がります。たとえば初回表示のチラつきや、フォーム送信の取りこぼしです。すぐに致命傷には見えなくても、1年単位では信頼低下につながる可能性があります。

  • 調査時間の増大 — 毎回ゼロから調べるので、障害1件あたりの工数が膨らきやすいです。
  • 説明コストの増大 — 非エンジニア向け説明が属人化し、意思決定が遅れやすいです。
  • 再発率の上昇 — 根本原因が残り、別画面で同系統の不整合が再登場しやすいです。

この記事を書いている理由

僕自身、2025年10月に結婚式Web招待状サービスを自作して即リリースし、90日以上ログイン継続率を維持しながら改善を回してきました。そのとき痛感したのが、障害対応は「気合い」より「手順」のほうが強いということです。今は福祉事業のIT全般を担当していて、非ITの現場言語で説明しながら復旧する場面が多いです。だからこそ、ITの武器を非IT領域に持ち込む異世界転生の視点で、再現しやすいRunbookとして共有したいと思っています。

次のアクション

運用メモとして、外部導線ではなく、実測ログ・設定差分・再現手順を同じ場所に残してチーム内で確認できる形にしてください。

今日からできるアクション

  • 設定するlogging.serverFunctionsbrowserToTerminal を有効化します。
  • 記録する — Incident Logテンプレートをリポジトリに追加します。
  • 回遊する — 関連記事の比較記事実践手順記事も合わせて読むと定着が早いです。