結論
Next.js App Router で body タグに身に覚えのない cz-shortcut-listen="true" が混入して Hydration mismatch が出ている場合、原因はあなたのコードではなく ブラウザ拡張機能 ColorZilla です。対処は src/app/layout.tsx の <body> に suppressHydrationWarning を 1 行付けるだけで済みます。
// src/app/layout.tsx
<body className="..." suppressHydrationWarning>
{children}
</body>
ただし「suppressHydrationWarning をどこに、どれだけの範囲で付けてよいのか」は判断を伴うため、本文の終盤でメリット・デメリットと代替案を整理します。
検証環境
| 項目 | バージョン |
|---|---|
| OS | macOS (Darwin 25.5.0) |
| Chrome | 149.0.7827.54 (arm64) |
| ブラウザ拡張 | ColorZilla 4.1 |
| Next.js | 16.2.5 |
| React | 19.2.4 |
自分の環境では ColorZilla 4.1 が body に属性を注入していることまで特定できました。Chrome 拡張のうち、ショートカット待受用に body へ属性を差し込む系の拡張(Compose AI、ChatGPT Writer、Grammarly 等)を入れている場合は同じ症状に至る可能性があります。
エラーメッセージと差分
ブラウザのコンソールに出るエラー全文は次のとおりです(検索でこの記事にたどり着いた方のためにコードブロックで貼っておきます)。
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.
This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
そしてその下に、サーバーレンダリングとクライアントの差分が次のように表示されます。
+ Client - Server
<body
className="min-h-full flex flex-col"
+ cz-shortcut-listen="true"
>
着目すべきは cz-shortcut-listen="true" の 1 行です。自分が書いた layout.tsx のどこにも存在しない属性が、クライアント側にだけ生えています。
なぜ「ブラウザ拡張」と判別できるのか
Hydration mismatch の主因は大きく分けて 5 パターンあります。
-
Date.now()/Math.random()のようにサーバーとクライアントで値が変わる式 -
window.innerWidthなどブラウザ専用 API への直アクセス - ユーザーの locale 依存(
new Date().toLocaleString()等) -
localStorageを初回レンダーで読む(SSR ではnull、再 hydrate 後に値あり) - ブラウザ拡張機能による属性注入(今回)
このうち 1〜4 は自分の書いたコードを読み返せば心当たりがあるはずです。一方、拡張機能ケースは次のような特徴で見分けがつきます。
- 差分に出る属性名が自分のソースのどこにも存在しない(
cz-shortcut-listenでリポジトリ全体を grep しても 0 件) -
bodyやhtmlなどルートに近い要素で起きる(拡張は document の上層を改変する作りが多い) - 同じコードを別ブラウザやシークレットモード(拡張無効)で開くと再現しない
裏返すと、最初の見分けとしては「差分に出た属性名で自分のコードを grep する」が手っ取り早い切り分け手段です。0 件ならまず拡張機能を疑って構いません。
犯人を ColorZilla 4.1 に絞り込んだ手順
「ブラウザ拡張のどれかが犯人」までは前章のチェックで分かります。ここからは特定の拡張に絞り込むまでの手順です。自分は次の順で進めました。
1. シークレットモードで再現を確認
Chrome のシークレットウィンドウは、拡張機能がデフォルトで無効化された状態で立ち上がります。同じローカル開発環境を開いて Hydration エラーが 出なくなれば、原因が拡張機能であることが確定します。逆に出続けるなら、拡張機能以外の原因を疑う段階に戻る必要があります。
2. 拡張機能を 1 つずつ無効化
chrome://extensions/ を開いてインストール済みの拡張機能を上から無効化していきます。1 つ無効化するごとに開発サーバーをリロードし、エラーが消えた瞬間に手を止めます。自分の環境では ColorZilla 4.1 を無効化した時点でエラーが消えました。
3. 単独で再現を再確認
無効化した順序の影響を排除するため、すべての拡張を無効化した状態から ColorZilla 4.1 だけを有効化して再確認します。これで cz-shortcut-listen="true" が再び出現すれば、犯人として確定です。
4. 属性名の裏取り
cz-shortcut-listen の cz は ColorZilla の頭文字で、ColorZilla の拡張機能がキーボードショートカットを待ち受けるために body 直下に挿入する属性です。同様に cz プレフィックスを使うのは ColorZilla 由来とされています。
ここまで来れば「自分のコードのバグではない」と確信して、対処に進めます。
対処: <body> に suppressHydrationWarning を付ける
特定が済んだら対処は 1 行です。src/app/layout.tsx の <body> に suppressHydrationWarning を追加します。
// src/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body className="min-h-full flex flex-col" suppressHydrationWarning>
{children}
</body>
</html>
)
}
suppressHydrationWarning は React が用意している hydration mismatch の警告抑制フラグで、付与した要素の 属性とテキスト内容の差異だけ を「警告として表示しない」扱いにします。重要な性質は次のとおりです。
- 抑制は その要素 1 つに対してのみ 効きます。
<body>に付けても、子コンポーネント側で発生する mismatch は引き続き検知・警告されます。 - hydration 自体をスキップするわけではありません。React は通常どおりサーバー HTML をクライアントの React ツリーに紐付け、警告だけを黙らせます。
- 効くのは React 公式が「拡張機能や日時表示など、サーバーとクライアントで差異が出ても許容したい既知のケース」に対する逃げ道としてです。
これだけで Hydration エラーは消え、ColorZilla を有効化したまま開発を続けられるようになります。
suppressHydrationWarning というアプローチのメリデメ
「拡張機能が原因なら、拡張機能の問題であって自分のアプリのコードを触る必要はないはず」という違和感は私自身も持ちました。ここでは、それでも <body suppressHydrationWarning> を付けるという選択をどう正当化するかを、メリット・デメリットと代替案の比較で整理します。
メリット
- 変更が 1 行で済む: アプリの設計やレンダリング戦略には一切影響しません。Server Component や Client Component の境界、SSR の挙動、ストリーミングのいずれも変わりません。
-
抑制の範囲が
<body>だけに閉じる:suppressHydrationWarningは付与した要素 1 つにしか効かないため、子コンポーネント側で起きる hydration mismatch は変わらず検知されます。「拡張機能が触る body の属性差だけ無視、それ以外の本物の不整合は引き続き拾う」という、ピンポイントな抑制になります。 - 開発体験への副作用が小さい: コンソールから恒常的なノイズが消えるため、本物のエラーが発生したときに気付きやすくなります。
デメリット
-
将来「body の属性差」で起きる別種の本物のバグも黙る: たとえば自分のコードがミドルウェアやサードパーティ製のスクリプトで body に属性を追加するようになった場合、それも警告なしに通過します。
<body>というスコープは狭いものの、ゼロではないことに注意が必要です。 -
拡張機能の挙動に依存した「沈黙」になっている: ColorZilla 側の更新で別の場所(
htmlや子要素)に属性を挿し始めた場合、<body>へのsuppressHydrationWarningでは捕捉できません。あくまで「いまの ColorZilla 4.1 が body にしか触らない」という前提に乗っかった対処です。 - 問題の本質は自分側にない: 厳密に言えばこれは「拡張機能と SSR の相互作用」という外的要因です。それをアプリのコードで吸収しているため、原因をコードから読み取れません。コメントで意図を残しておかないと、後から見た人(あるいは未来の自分)が「これは何のための属性?」と詰まる可能性があります。
代替案との比較
| 選択肢 | 抑制範囲 | 副作用 | 採否 |
|---|---|---|---|
| 何もしない(警告を放置) | なし | dev のコンソールにノイズが残り、本物のエラーを見落とす可能性 | 採用しづらい |
| 拡張機能を全部無効化する | 抑制ではなく根絶 | 自分のブラウザ環境を運用で縛ることになる。他の開発者・将来の自分には強制できない | 採用しづらい |
ルート全体を 'use client' にする |
全要素 | SSR を実質放棄。表示速度・SEO・ストリーミング全てが劣化 | 採用しない |
next/dynamic({ ssr: false }) で逃がす |
対象コンポーネント | 本ケースは body 直下の属性差なので、そもそも適用先がない | 該当しない |
<body suppressHydrationWarning>(本稿の対処) |
body 要素 1 つ | 上記デメリット | 採用 |
判断基準
私は次の 3 つが同時に成立する場合に限って、<body suppressHydrationWarning> を選んでよいと判断しています。
- mismatch が
body(またはhtml)で起きていて、子要素では起きていない - 差分の属性が自分のソースに存在しない(grep で 0 件)
- 再現が「拡張機能を入れた特定のブラウザ」に限られる(シークレットモード等で消える)
この 3 つが揃わない場合は、原因が拡張機能ではなく自分のコード側にある可能性が高いので、suppressHydrationWarning で蓋をする前に動的値・locale・localStorage 等の心当たりを潰すべきです。
補足: Hydration mismatch パターン早見表
「拡張機能ケース」以外で hydration mismatch に遭遇した方向けに、典型パターンと対処の早見表を残しておきます。
| 原因 | 典型例 | 対処 |
|---|---|---|
| サーバーとクライアントで値が変わる式 |
Date.now(), Math.random()
|
useEffect で初期値固定 → クライアント側で更新 |
| ブラウザ専用 API への直アクセス |
window.innerWidth, navigator.*
|
useEffect で読む / next/dynamic の { ssr: false }
|
| locale 依存の表示 | new Date().toLocaleString() |
locale 情報をサーバー側で確定し props で渡す |
localStorage を初回レンダーで参照 |
テーマ設定の即時反映 | クライアント側に状態を持たせ、初回は SSR と一致する初期値で描画 |
| ブラウザ拡張による属性注入 |
cz-shortcut-listen, data-grammarly-* 等 |
該当要素に suppressHydrationWarning
|
おわりに
Hydration エラーは「自分のコードのどこかが悪い」と思い込んで延々と見直しがちですが、差分の属性名が自分のソースに存在しないなら、原因はブラウザ拡張側の可能性が高いです。シークレットモードで再現確認 → 拡張を 1 つずつ無効化 → 単独再現、という 3 ステップで犯人はすぐに絞り込めます。
suppressHydrationWarning は便利な抑制フラグですが、その対象要素以下の本物の mismatch を見えなくしてしまう副作用を持ちます。今回のように「body の属性差分だけ無視したい」という極めて限定的な用途であれば妥当な選択ですが、適用範囲は常に最小に保つことを意識しておくのがよいと考えています。