惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
小众软件
小众软件
WordPress大学
WordPress大学
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
美团技术团队
Jina AI
Jina AI
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
D
Docker

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
React 19 Hydration Mismatch in Static Export
Mark · 2026-06-14 · via DEV Community

Mark

📚 This is Part 3 of the UtlKit Tech Series
Part 2 covers the architecture & trade-offs → Read Part 2

React 19 Static Export Hydration Mismatch? Pitfalls in Next.js 15

I built a 150+-tool online site with Next.js 15, deployed on Cloudflare Pages.
Everything looked great until the console started flooding with Hydration Error #418.

The Problem

Open DevTools Console — a wall of red:

Error: Hydration failed because the server rendered HTML didn't match the client.
Expected server HTML: <html class="dark">
Client-rendered HTML: <html>

The site works fine functionally, but this error hurts SEO scores and floods Sentry.

Weirder still: npm run dev works fine. npm run build && npm start breaks.

Root Cause Analysis

Layer 1: React 19 RSC and Static Export Compatibility

Next.js 15 defaults to React 19 with React Server Components (RSC). But in output: 'export' mode, every page is statically rendered to HTML.

The problem:

  1. SSR phase: Server-side render tries to read localStorage (theme value) → can't read it (no localStorage on server)
  2. CSR phase: Client-side hydration reads localStorage → has a value, dynamically adds class="dark"
  3. Result: SSR HTML ≠ CSR HTML → Hydration Mismatch

Layer 2: Dynamic className on <html> and <body>

The layout.tsx had code like this:

// ❌ Causes Hydration Mismatch
export default function RootLayout({ children }) {
  const theme = useTheme() // Client hook reading localStorage
  return (
    <html lang="zh" className={theme}>
      <body>{children}</body>
    </html>
  )
}

useTheme() returns the default value (light) during SSR, but returns the localStorage value (possibly dark) during CSR. Different class on <html> → Hydration Error.

Layer 3: Terser Made Things Worse

To reduce bundle size, I added Terser minification to the build:

// next.config.js
compress: true,

After Terser compression, code execution timing changed subtly, making hydration behave inconsistently across browsers (especially Chrome). Sometimes it hydrates fine, sometimes it errors — non-deterministic bugs are the worst.

Solutions (Progressive)

Solution 1: suppressHydrationWarning (Quick Fix)

// ✅ Tells React to ignore differences on <html>
<html lang="zh" className={theme} suppressHydrationWarning>

Result: Errors gone ✅
Problem: This masks the issue. Dark mode still flashes on first paint — white then black.

Solution 2: Head Script to Pre-read Theme (Eliminates Flash)

Add a <script> in <head> that executes before React renders:

<script>
  (function() {
    const theme = localStorage.getItem('theme') || 'light';
    document.documentElement.classList.add(theme);
    document.documentElement.style.colorScheme = theme;
  })();
</script>

Result: No first-paint flash ✅, suppressHydrationWarning clears errors ✅
Problem: Good enough, but I wanted to try React 19...

Solution 3: Downgrade React 19 → 18 (Root Fix)

After testing, React 19's RSC has several known issues with output: 'export':

  • Different hydration timing than React 18
  • Some Server Component features behave unexpectedly during static export
  • Many community reports (GitHub Issues)

Downgraded decisively:

// package.json
{
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  }
}

Result: All Hydration issues completely resolved ✅
Problem: Lost RSC, but static sites don't need it anyway.

Final Solution (What's in Production)

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="zh" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: `
          (function() {
            try {
              var t = localStorage.getItem('theme') || 'light';
              document.documentElement.classList.add(t);
              document.documentElement.style.colorScheme = t;
            } catch(e) {}
          })();
        `}} />
      </head>
      <body>{children}</body>
    </html>
  )
}

Key points:

  1. <script> in <head> executes before React → eliminates flash
  2. suppressHydrationWarning → eliminates errors
  3. try/catch → prevents localStorage errors in restricted environments (iframes, etc.)
  4. React 18 → stable hydration timing

Lessons Learned

Approach Effect Recommendation
suppressHydrationWarning only Clears errors, but flash remains ⭐⭐
Head script + suppressHydrationWarning No errors, no flash ⭐⭐⭐⭐
React 19→18 downgrade + head script Most stable ⭐⭐⭐⭐⭐
useLayoutEffect delayed DOM Works but complex ⭐⭐⭐

Core takeaways:

  1. Static sites don't need RSC — RSC brings 10x more problems than benefits in output: 'export' mode
  2. localStorage is the #1 cause of Hydration mismatches — SSR can't read it, CSR can, guaranteed inconsistency
  3. Head script is the best pattern for theme/locale flash — Zero dependencies, zero overhead
  4. Don't over-optimize builds — Terser saved a few KB but introduced non-deterministic bugs

Project

This site is UtlKit — 150+ free online developer tools. All the issues above were encountered during real development and deployment, and the solutions are running stably in production.


If this helped, feel free to leave a ❤️. Questions welcome in the comments.