Next.js · Performance
Hydration Failures in Next.js
Fix React hydration mismatches in the Next.js App Router that break interactivity and SEO trust.
Problem Overview
Hydration is when the client JavaScript attaches to server-rendered HTML. A hydration failure (or mismatch warning) means the DOM the server sent does not match what React expected on the client. Buttons stop working, content flickers, and crawlers may see an unstable or incomplete page.
Why It Matters
Broken hydration is a conversion and performance Money Gap™: high-intent pages look fine in a static screenshot, then fail when users click. Mismatches also inflate INP/CLS and erode trust in SEO/AI crawlers that rely on a consistent first paint.
Treat revenue impact as an AI Estimate only — never claim guaranteed ROI from fixing hydration alone.
Framework-Specific Explanation
In Next.js (App Router), Server Components render HTML on the server; Client Components ("use client") hydrate on the browser. Mismatches usually come from Client Components that render different trees on server vs client, or from invalid nesting inside the shared document shell.
Prefer keeping marketing heroes and primary CTAs in Server Components. Push interactivity (modals, charts) into small Client islands with identical initial markup.
Step-by-Step Solution
- Reproduce with
next devand watch the console for “Text content did not match” / hydration errors. - Isolate the Client Component that owns the mismatch; temporarily simplify its first render to static text.
- Move time/locale/random values into
useEffect(post-hydrate) or pass them as props from a stable server source. - Validate HTML: no
<p>wrapping block elements; use the Next.js docs invalid HTML guide. - Re-test production build (
next build && next start) — some mismatches only appear outside Strict Mode quirks. - Re-scan with
npx moneygap-scan <url>and confirm CTAs still work.
Code Examples
// Bad: different server vs client first paint
"use client";
export function Greeting() {
return <p>Today is {new Date().toLocaleDateString()}</p>;
}
// Better: stable first paint, then enhance
"use client";
import { useEffect, useState } from "react";
export function Greeting() {
const [label, setLabel] = useState("Today");
useEffect(() => {
setLabel(`Today is ${new Date().toLocaleDateString()}`);
}, []);
return <p>{label}</p>;
}
For App Router metadata, keep titles in generateMetadata / metadata exports — never set document.title only on the client for SEO-critical pages.
Common Mistakes
- Rendering
Date.now(),Math.random(), or locale-dependent strings differently on server vs client - Branching on
typeof window !== "undefined"during the first render - Invalid HTML nesting that browsers “fix” before React hydrates
- Browser extensions or third-party scripts mutating the DOM before hydrate
- Using client-only libraries without a stable server placeholder
Validation Checklist
- [ ] No React hydration warnings in the browser console on key routes
- [ ] Interactive CTAs work immediately after load (no dead clicks)
- [ ] View Source HTML matches the critical above-the-fold text users see
- [ ] Lighthouse / field data show no unexplained CLS from content swap
- [ ]
moneygap scan/ sandbox diagnostics still pass crawl and schema checks after the fix
AI Readiness Notes
Stable SSR HTML helps answer engines cite accurate copy. Prefer server-rendered titles, headings, and primary CTAs so AI crawlers never depend on a failed hydrate to discover your offer.
Deployment Checklist
- [ ] Zero hydration warnings on
/, pricing, and signup routes in production - [ ] Primary CTA (“Start Free Trial”) clickable without a full client remount
- [ ] Preview deployments checked with View Source + console
- [ ]
moneygap-scanrun against the preview URL before merge
Browser Extension Tips
After deploy, open the live URL in the MoneyGap extension and confirm no conversion/interactivity findings that look like “dead” CTAs caused by hydrate failures.