π My Traffic Dropped to 0 overnight: The Next.js 15 Hydration Trap
Imagine waking up, checking your Google Analytics 4 (GA4) dashboard for your shiny new SaaS product, and seeing a horrifying number: 0 Users. 0 Views. 100% Drop.
Did the servers crash? Did Google de-index my domain?
Neither. The site was running perfectly fine. The culprit? A sneaky Hydration Mismatch in Next.js 15 that silently murdered my tracking script.
Here is how a seemingly innocent<GoogleAnalytics /> component placement caused a complete tracking blackout on sandagent.dev, and how you can avoid this exact trap.
π΅οΈ The Crime Scene
Like any good Next.js developer, I wanted to add Google Analytics to my app/layout.tsx. Standard procedure, right? I used a third-party GA package (or standard next/third-parties/google) and placed it right where it belongsβin the <head> tag.
// β The Deadly Mistake
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Looks perfectly normal, doesn't it? */}
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</head>
<body>
{children}
</body>
</html>
);
}
π The Investigation: Why it broke
In Next.js 15 (with React 19), the hydration process has become incredibly strict.
When you place dynamic script components inside the <head>, the server renders the HTML with the injected <script> tags. However, during the client-side hydration phase, third-party browser extensions, or even React's own strict <head> reconciliation, can cause a mismatch.
Instead of just throwing a red warning in your console and moving on, the hydration failure caused React to effectively drop or bypass the execution of the GA tracking scripts in the client-side DOM tree.
The result? The page visually loads perfectly, the user clicks around, but the collect?v=2 network request is never sent to Google. Complete data blackout.
π οΈ The Fix (The 1-Line Solution)
After digging through the Next.js docs and debugging the React tree, the fix was almost embarrassingly simple.
Do not put the GA component in the <head>. Put it inside the <body>.
// β
The Correct Way
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Leave standard meta tags here */}
</head>
<body>
{children}
{/* Place it here instead! */}
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</body>
</html>
);
}
Why does this work?
By placing the script tag inside the <body> (or at the very end of it), it avoids conflicting with React's strict <head> management during the initial render pass. The script still loads asynchronously, performance isn't impacted, but most importantly: React hydration no longer swallows your tracking code.
π‘ Takeaways for Next.js 15 Developers
-
Don't trust the visual load: Just because your site didn't 500 error doesn't mean your background scripts are running. Check your Network tab for
collectrequests after a major Next.js version bump. -
Move scripts to
<body>: Unless strictly required by the provider to be the first thing in the<head>, placing analytics components inside the body tag is much safer against React 19 hydration mismatches. - Set up traffic anomaly alerts: If I hadn't had an automated cron job fetching daily GA reports, I might have gone weeks without realizing my traffic was zeroed out.
Have you run into weird React 19 / Next.js 15 hydration bugs yet? Let me know in the comments!
(P.S. If you're building AI agents, you can check out the project that almost lost all its metrics at sandagent.dev π)
*
Top comments (0)