A React hydration error that only showed up in one production build
React error #418 on roughly one in eight to one in four page loads, but only in one build. How I measured it, bisected it and fixed it.
After switching my production build to Webpack, my site started logging this in the browser console on some page loads:
Minified React error #418
That is a hydration error: the HTML the server sent did not match what React rendered in the browser. It did not happen every time, which made it harder to catch. This is how I tracked it down, and what I could and could not conclude.
First, measure it
A single clean run proved nothing. My first automated browser run passed all 18 checks, and the next runs failed one or two. So I wrote a small Playwright script that:
- opens a page in a single tab, then navigates to the next page in the same tab, the way a real visitor does;
- counts any page error that matches React error 418;
- repeats the whole loop many times, so the result is a rate instead of a feeling.
On the broken build the rate came out between roughly one in eight and one in four page loads, depending on the run. It mostly hit blog and admin pages.
Narrowing where it happens
The most useful result was where it did not happen:
- not in the development server;
- not in a Turbopack production build of the same code;
- not in the Next.js 14 build that was live before;
- not in a minimal Next.js 16 app built with Webpack.
So Webpack alone was not the cause. It was my code combined with how Webpack's production output loads, and repeat visits in the same tab.
Bisecting my own layout
I stripped the root layout to the bare minimum (just <html>, <body> and the children) and the errors disappeared. Then I put pieces back one at a time and re-ran the loop each time:
- Everything except the theme provider: errors came back.
- The shell component with its URL check removed: fewer errors, not zero.
- Everything except the chat widget: fewer errors, not zero.
No single removal fixed it, so I did not find one smoking-gun line, and I want to be clear about that. What the experiments did show is that two things in the root layout were involved: a client component that used usePathname() to decide between two different page structures (public pages versus admin pages), and a chat widget that I could move entirely to the client, since it is interactive-only.
The fix: remove the ambiguity
Instead of one component guessing the page structure from the URL, I made the structure explicit with Next.js route groups:
- public pages live in
app/(site)/with their own layout that renders the header, footer and sidebar; - admin pages keep their own layout;
- the root layout only renders
<html>,<body>and the theme provider; - the shell is now a server component with no URL logic;
- the chat widget loads on the client only, through
next/dynamicwithssr: false, since it is interactive-only and adds nothing to the HTML a search engine needs.
URLs did not change, because route groups are folders in parentheses that stay out of the path.
Result
I re-ran the same loop across every public page, both kinds of 404, and every admin page. After the change there were zero hydration errors in 144 page loads, against the earlier rate of one in eight to one in four.
Lessons
- Measure the rate. An intermittent bug needs a loop and a count, not one passing run.
- Compare environments. Knowing where it did not happen (dev, Turbopack, the old version) narrowed the search more than staring at code.
- Be wary of a client component in the root layout that changes structure based on the URL. Separate layouts are easier to reason about, and they keep more of the page on the server.
- Test with the same bundler your host will use.
If you want a developer who tests production builds this carefully, see my portfolio or get in touch.