Next.js „Hydration failed“: Ursachen und Lösungen

Was hinter „Hydration failed“ in Next.js steckt, wie Sie die Ursache eingrenzen und welche Lösungen sauber sind – mit Codebeispielen für den App Router.

Veröffentlicht
Aktualisiert
Autor
Mario Gvozden
Lesezeit
7 Minuten

Die Meldung erscheint im Browser, oft als rotes Overlay im Entwicklungsmodus: „Hydration failed because the server rendered HTML didn't match the client.“ In älteren React-Versionen hieß sie noch „Text content does not match server-rendered HTML“. Die Seite funktioniert danach meistens trotzdem, und genau deshalb bleibt der Fehler gern monatelang liegen. Das ist keine gute Idee: React verwirft bei einem echten Hydration-Fehler den betroffenen Teilbaum und rendert ihn im Browser neu. Das kostet Ladezeit, kann sichtbares Flackern verursachen und verdeckt manchmal Fehler, die später an anderer Stelle auftauchen.

Was Hydration eigentlich ist

Next.js rendert eine Seite zuerst auf dem Server zu HTML. Der Browser zeigt dieses HTML sofort an. Danach lädt das JavaScript, und React rendert dieselben Komponenten im Browser ein zweites Mal, um Event-Handler an das vorhandene HTML zu hängen. Diesen Abgleich nennt man Hydration.

Die Voraussetzung dafür: Beide Durchläufe müssen exakt dasselbe Markup erzeugen. Weicht der Browser-Durchlauf ab, kann React das HTML nicht übernehmen und meldet den Fehler. Fast jede Ursache lässt sich auf eine Frage zurückführen: Was ist auf dem Server anders als im Browser?

Die häufigsten Ursachen

1. Werte, die sich zwischen Server und Browser unterscheiden

Der Klassiker sind Datum und Uhrzeit. Der Server läuft oft in UTC, der Browser in der Zeitzone des Besuchers. Ähnlich problematisch sind Math.random(), Date.now(), zufällige IDs oder Zahlenformatierung mit toLocaleString() ohne feste Locale.

// Problem: Server und Browser formatieren unterschiedlich
export function Stand() {
  return <p>Stand: {new Date().toLocaleTimeString()}</p>;
}

Wenn der Wert nur im Browser korrekt sein kann, rendern Sie ihn erst nach dem Mounten:

"use client";
import { useEffect, useState } from "react";

export function Stand() {
  const [zeit, setZeit] = useState<string | null>(null);

  useEffect(() => {
    setZeit(new Date().toLocaleTimeString("de-DE"));
  }, []);

  return <p>Stand: {zeit ?? "…"}</p>;
}

Für eindeutige IDs, etwa für htmlFor und id in Formularen, gibt es useId() aus React. Die ID ist auf Server und Client identisch.

2. Abfragen von window, localStorage oder navigator beim Rendern

// Problem: auf dem Server ist window undefined
const istMobil = typeof window !== "undefined" && window.innerWidth < 768;

Der Server rendert hier die Desktop-Variante, der Browser die mobile. Das gleiche Muster tritt bei Themes aus localStorage, bei Cookie-Bannern oder bei „Zuletzt angesehen“-Listen auf. Auch hier gilt: Browser-Zustand gehört in useEffect oder in eine Komponente, die nur im Browser geladen wird. Für reine Darstellungsfragen wie die Bildschirmbreite ist CSS (Media Queries) meist die bessere Lösung, weil sie gar keine Abweichung erzeugt.

3. Ungültig verschachteltes HTML

Ein <div> innerhalb eines <p>, ein <a> in einem <a>, ein <tr> ohne <tbody>: Der Browser korrigiert solches HTML beim Parsen selbstständig und baut eine andere DOM-Struktur, als React erwartet. React zeigt in neueren Versionen in der Fehlermeldung einen Diff an, in dem Sie die Stelle oft direkt erkennen.

// Problem: <div> darf nicht in <p> stehen
<p>
  <div className="hinweis">Versandkostenfrei ab 50 €</div>
</p>

Häufig steckt das Problem in einer Komponente, die intern ein <div> rendert und in einem Absatz verwendet wird, etwa ein Badge oder ein Tooltip. Ersetzen Sie das äußere <p> durch ein <div> oder das innere Element durch ein <span>.

4. Browser-Erweiterungen

Passwortmanager, Übersetzungs- und Rechtschreib-Erweiterungen fügen dem <body> oder <html> eigene Attribute hinzu, bevor React hydriert. Das erzeugt eine Abweichung, die mit Ihrem Code nichts zu tun hat. Prüfen Sie den Fehler deshalb immer auch in einem privaten Fenster ohne Erweiterungen.

Tritt er nur mit Erweiterung auf, ist suppressHydrationWarning auf genau diesem Element vertretbar:

// app/layout.tsx
<html lang="de" suppressHydrationWarning>
  <body>{children}</body>
</html>

Wichtig: Das Attribut wirkt nur eine Ebene tief, auf Attribute und Textinhalt des Elements selbst. Es ist kein Schalter, um Hydration-Fehler im ganzen Baum stummzuschalten, und sollte auch nicht so verwendet werden.

5. Daten, die sich zwischen den Durchläufen ändern

Wenn eine Client Component Daten selbst lädt oder auf einen globalen Store zugreift, der im Browser bereits anders befüllt ist als auf dem Server, entstehen ebenfalls Abweichungen. Typisch sind Warenkörbe aus localStorage oder Feature-Flags, die im Browser anders ausgewertet werden. Übergeben Sie in solchen Fällen die Daten vom Server als Props, statt sie im Browser neu zu ermitteln, oder rendern Sie den Teil bewusst erst nach dem Mounten.

6. Fehlerhafte Drittanbieter-Skripte

Skripte, die das DOM vor der Hydration verändern, etwa Tag-Manager oder Chat-Widgets, die Elemente in vorhandene Container schreiben. Binden Sie solche Skripte über next/script mit strategy="afterInteractive" oder "lazyOnload" ein und geben Sie ihnen einen eigenen Container, den React nicht rendert.

Vorgehen bei der Fehlersuche

  1. Fehler reproduzieren. Seite im privaten Fenster öffnen, Konsole aufklappen. Notieren Sie die genaue Meldung und den Diff, den React anzeigt.
  2. Seitenquelltext vergleichen. „Seitenquelltext anzeigen“ zeigt das Server-HTML, der Elemente-Tab in den DevTools das DOM nach der Hydration. Die Abweichung ist häufig auf den ersten Blick sichtbar.
  3. Eingrenzen. Kommentieren Sie Komponenten auf der betroffenen Seite schrittweise aus, bis der Fehler verschwindet. Bei verschachtelten Layouts zuerst die Komponenten mit Datum, Zufall oder Browser-APIs prüfen.
  4. Ursache beheben, nicht verstecken. suppressHydrationWarning und das Abschalten von Server-Rendering lösen das Symptom, nicht die Ursache.
  5. Produktions-Build testen. next build && next start lokal ausführen. Manche Abweichungen, etwa durch Minifizierung oder unterschiedliche Umgebungsvariablen, zeigen sich nur dort.

Wann ssr: false vertretbar ist

Es gibt Komponenten, die auf dem Server keinen Sinn ergeben: eine Karte, die auf window angewiesen ist, ein Editor, ein Diagramm aus einer reinen Browser-Bibliothek. Dafür ist das Laden ohne Server-Rendering legitim:

"use client";
import dynamic from "next/dynamic";

const Karte = dynamic(() => import("./Karte"), {
  ssr: false,
  loading: () => <div className="h-80" />,
});

export function Standort() {
  return <Karte />;
}

Beachten Sie zwei Punkte. Erstens ist ssr: false im App Router nur in Client Components erlaubt, nicht in Server Components. Zweitens verliert der Inhalt dadurch sein HTML im ersten Ladevorgang. Für Text, der für Suchmaschinen wichtig ist, ist das die falsche Lösung. Geben Sie dem Platzhalter außerdem eine feste Höhe, damit die Seite beim Nachladen nicht springt.

Checkliste

  • Fehler im privaten Fenster ohne Erweiterungen reproduziert
  • Server-HTML und DOM verglichen
  • Datum, Zufallswerte und Locale-Formatierung geprüft
  • Zugriffe auf window, localStorage, navigator während des Renderns entfernt
  • Verschachtelung von <p>, <a>, Tabellen und Listen geprüft
  • suppressHydrationWarning nur dort, wo eine Abweichung erwartet und harmlos ist
  • Produktions-Build lokal getestet, Konsole ohne Warnung

Was übrig bleibt

Oft ist die Ursache eine einzelne Stelle: eine Datumsformatierung, ein verschachteltes Element, ein Check auf window. Schwierig wird es, wenn mehrere Ursachen zusammenkommen oder der Fehler nur in Produktion und nur bei bestimmten Nutzern auftritt. Dann lohnt sich ein systematischer Blick, bevor an Symptomen gearbeitet wird.

Häufige Fragen

Ist „Hydration failed“ schlimm, wenn die Seite trotzdem funktioniert?

Ja, der Fehler sollte behoben werden. React verwirft bei einem echten Hydration-Fehler den betroffenen Teilbaum und rendert ihn im Browser neu. Das kostet Ladezeit, kann sichtbares Flackern verursachen und verdeckt manchmal Fehler, die später an anderer Stelle auftauchen.

Darf ich suppressHydrationWarning verwenden?

Nur gezielt, etwa auf dem html-Element, wenn Browser-Erweiterungen Attribute hinzufügen. Das Attribut wirkt nur eine Ebene tief, auf Attribute und Textinhalt des Elements selbst. Als Schalter, um Hydration-Fehler im ganzen Baum stummzuschalten, ist es weder gedacht noch geeignet.

Wie finde ich die Ursache eines Hydration-Fehlers?

Reproduzieren Sie den Fehler zuerst in einem privaten Fenster ohne Erweiterungen und notieren Sie den Diff, den React anzeigt. Vergleichen Sie dann den Seitenquelltext mit dem DOM in den Entwicklertools und kommentieren Sie verdächtige Komponenten schrittweise aus. Datum, Zufallswerte und Zugriffe auf window sind die ersten Kandidaten.


Einen Hydration-Fehler beheben wir als eine Aufgabe auf Ihrem Board: Sie laden uns in Ihr Repository ein; sobald wir starten, stehen die Abnahmekriterien auf der Karte, und 48 Stunden nach Start liefern wir die Korrektur als Pull Request mit Erklärung der Ursache – gezählt von Montag 8 Uhr bis Freitag 16 Uhr. Bevor der Pull Request zu Ihnen geht, prüft ihn ein Senior-Entwickler und gibt ihn frei; über den Merge entscheiden Sie. Die erste Aufgabe erledigen wir kostenlos, eine pro Unternehmen: Erste Aufgabe anlegen. Weitere Korrekturen und Erweiterungen setzen wir im Prozess-Abo um; mehr dazu unter Fehler beheben und im Leistungskatalog.

Nächster Schritt
  • Die erste Aufgabe ist kostenlos – eine pro Unternehmen. Sie legen einen Workspace an, geben Zugang per Einladung und schreiben die Aufgabe auf Ihr Board. Wir liefern sie 48 Stunden nach Start, gezählt Mo 8 bis Fr 16 Uhr: Start am Montag um 10 Uhr – Lieferung am Mittwoch um 10 Uhr. Jede Lieferung prüft und gibt ein Senior-Entwickler frei, bevor sie zu Ihnen geht.

  • Danach im Prozess-Abo: Sie schreiben Aufgaben aufs Board und bestimmen die Reihenfolge, wir setzen sie um. Prozess-Abo 2,5: 2.500 € im Monat, 1 Aufgabe gleichzeitig in Arbeit, bis zu 4 Aufgaben pro Monat; Prozess-Abo 5: 5.000 € im Monat, 1 Aufgabe gleichzeitig in Arbeit; Prozess-Abo 10: 10.000 € im Monat, 2 Aufgaben parallel in Arbeit. Gezählt werden die Aufgaben, die wir im Abrechnungsmonat beginnen; eine Nachbesserung zählt nicht neu. Nicht genutzte Aufgaben verfallen am Monatsende. Preise zzgl. MwSt., nur für Unternehmen, monatlich kündbar – das Abo beginnt nur, wenn Sie es bestellen.

  • Fragen vorab oder mehr Aufgaben parallel? Im kostenlosen Projekt-Check (30 Minuten) klären wir das, auch für ein individuelles Abo.