#frontend #javascript #vuejs

Hydration Fehler in Vue.js verstehen und lösen

Eines vorweg: Dieser Artikel und das Wissen ist bei uns im Laufe der letzten Jahren zwar mit Vue.js entstanden, jedoch kann man die grundlegenden Erkenntnisse ebenfalls auf React-Anwendungen beziehen.

Für viele Entwickler stellt der Einstieg in Vue.js keine allzu großen Probleme dar. Neben einer hervorragenden und ständig erweiterten Dokumentation, gibt es ebenfalls eine große Community auf den verschiedensten Kanälen. Sollte man sich jedoch aus welchen Gründen auch immer dazu entscheiden, Server-Side-Rendering (SSR) zu betreiben, nimmt die Komplexität deutlich zu. Erfreulicherweise gibt es NuxtJS. Ein bereits seit einigen Jahren immer größer werdendes Vue.js framework, das einem u.a. eine robuste Implementierung des SSR zur Verfügung stellt. Dadurch wird einem den Einstieg deutlich erleichtert.

Früher oder später, ob ohne oder mit NuxtJS, wird man jedoch im Zuge der Entwicklung auf folgende Fehlermeldungen in der Browser Console stoßen:

Mismatching childNodes vs. VNodes

The client-side rendered virtual DOM tree is not matching server-rendered content.`

picture website

Im Production Kontext sieht die Meldung etwas anders aus.

HierarchyRequestError: Failed to execute 'appendChild' on 'Node': This node type does not support this method`

picture website
Diese Meldung entsteht während der sogenannten (Client-Side) Hydration, die im Browser stattfindet.

Was ist Hydration?

Die Dokumentation aus dem Vue SSR Guide beschreibt den Vorgang wie folgt:

Hydration refers to the client-side process during which Vue takes over the static HTML sent by the server and turns it into dynamic DOM that can react to client-side data changes.

Wenn der Browser eine URL aufruft, dann erhält dieser aufgrund des SSR das erzeugte HTML zurück und stellt dieses entsprechend dar.

picture website

Der Nutzer erhält bereits vor Beendigung der JavaScript-Ausführung im Browser eine lesbare Version der Web-App. Was fehlt sind die Reaktivität und die DOM-Event-Handler wie z.B. onclick. Für diesen Schritt muss das JavaScript im Browser noch ausgeführt werden, damit die Web-App vollständig nutzbar ist.

Im Development Mode wird sichergestellt, dass der clientseitig generierte virtuelle DOM mit der vom Server gerenderten DOM-Struktur übereinstimmt. Wenn es Unterschiede gibt, wird die Hydration abgebrochen, das vorhandene DOM verworfen und von Grund auf neu gerendert.

Im Production Mode ist dieses Vorgehen deaktiviert, um eine höhere Performance zu gewährleisten.

Wann tritt das Problem auf?

Wenn eine Komponente initial in den DOM gemounted bzw. hydrated wird, muss sie genau dem serverseitig generierten HTML entsprechen. Erst danach darf sich das HTML ändern. Ein geeigneter Zeitpunkt, der nach der Hydration liegt, wäre daher der mounted lifecycle.

Der Fehler kann auch nur bei Verwendung von SSR auftreten und nur dann, wenn eine URL direkt aufgerufen wird, sprich HTML vom Server zurückkommt. Bei einer reinen client-side-rendered Applikation wird dieser Fehler nicht auftreten.

Da die Hydration nur beim initialen Seitenaufruf durchgeführt wird, treten nachfolgend keine weiteren Fehler mehr auf und die Website verhält sich wie eine reine Single-Page-Application.

Welche Ursachen gibt es?

Wie bereits erwähnt wird sichergestellt, dass der in dem Browser erzeugte DOM-Baum mit dem vom Server gelieferten HTML übereinstimmt.

Vue will assert the client-side generated virtual DOM tree matches the DOM structure rendered from the server.

Folgende beiden HTML-Schnipsel produzieren jedoch einen Hydration Fehler!

<!-- Server -->
<div>foo</div>

<!-- Client -->
<div><div>foo</div></div>

Unterschiede in Attributen oder Text-Nodes führen nicht zu Fehlern.

<!-- Server -->
<div>foo</div>

<!-- Client -->
<div>bar</div>

Die Frage, die sich jetzt stellt ist, wie es denn zu diesen Unterschieden kommen kann, wenn es ja derselbe Vue.js Code ist, der einmal auf dem Server und einmal im Browser ausgeführt wird.

Grob gesagt sind es zwei Dinge, die dazu führen:

  • Eine ungültige HTML-Struktur oder
  • Unterschiedliche Zustände in Client/Server.

Ungültige HTML-Struktur

Durch den heute populären Ansatz der komponenten-getriebenen Entwicklung passiert es schnell, dass man ungültiges HTML über mehrere Komponenten hinweg unbewusst erzeugt.

Wie führt ungültiges HTML zu Hydration Fehlern?

Grundsätzlich ist das kein Problem bzgl. der Hydration Fehler. Das Problem ist, dass wir während der Hydration das in der Vue.js Applikation im Client erzeugte virtuelle DOM gegen das vom Browser interpretierte DOM aus dem HTML des Servers vergleichen. Das erzeugte HTML vom SSR stammt aus derselben virtual DOM Library namens snabbdom wie im Client. Der Unterschied ist nur, dass der Browser das HTML noch weiter verarbeitet hat. Und bekannterweise vergeben Browser recht viele Fehler im Aufbau der HTML-Struktur – was am Ende möglicherweise visuell zu derselben Darstellung führt, aber das DOM nicht identisch ist.

Ein paar Beispiele:

<!-- Verschachtelte Links -->
<a>foo<a>bar</a></a>

&lt;!-- <div&gt; in &lt;p&gt; -->
&lt;p&gt;&lt;/div&gt;foo&lt;/div&gt;&lt;/p&gt;

&lt;!-- <p&gt; in &lt;ul&gt; -->
&lt;ul&gt;&lt;p&gt;foo&lt;/p&gt;&lt;/ul&gt;

&lt;!-- v-if auf oberster Ebene --&gt;
&lt;template&gt;
   &lt;div v-if="true"&gt;foo&lt;/div&gt;
&lt;/template&gt;

&lt;!-- <table&gt; ohne &lt;tbody&gt; -->
&lt;table&gt;
   &lt;tr&gt;
      &lt;td&gt;foo&lt;/td&gt;
   &lt;/tr&gt;
&lt;/table&gt;

Wie lassen sich Hydration Fehler durch ungültiges HTML beheben?

Damit der Browser den DOM nicht eigenständig anpasst, muss jederzeit gültiges HTML zurückgegeben werden. Im <table> Beispiel oben müsste ein <tbody> ergänz werden, um die Hydration Fehler zu beheben.

Unterschiedliche Zustände in Client/Server

Unterschiedliche HTML-Strukturen können aus vielen unterschiedlichen Gründen entstehen. Am Ende dreht es sich aber immer um dasselbe Problem:

Wir vergleichen das Ergebnis (HTML) aus zwei Berechnungen (Rendering) zu unterschiedlichen Zeitpunkten mit unterschiedlichen Zuständen (Server, Client).

Wichtig hierbei ist zu beachten, dass diese Umstände bzw. dieser Code vor der Hydration stattfinden muss und es sich um Unterschiede in der HTML-Struktur handeln muss, um als möglicher Grund in Frage zu kommen. Solange nur der Inhalt von Text-Nodes / Attributen betroffen ist, d.h. keine Nodes hinzukommen oder wegfallen, führt dies nicht zu Hydration Fehlern.

Wie führen Unterschiede im State zu Hydration Fehlern?

Wenn in Server und Client dem DOM zum Zeitpunkt der Hydration ein unterschiedlicher Zustand vorliegt, können Nodes fehlen oder hinzukommen, was dann wie oben beschrieben zu Fehlern führt.

Folgender Code würde zu Hydration-Fehlern führen, da die data Funktion im Client erneut ausgeführt wird und für displayContent einen anderen Wert als auf dem Server bestimmen könnte. Dann würde sich der DOM zwischen Server und Client unterscheiden.

&lt;!-- index.vue --&gt;

&lt;template&gt;
   &lt;SomeContent v-if="displayContent" /&gt;
&lt;/template&gt;

&lt;script&gt;
export default %7B
   data() %7B
      return %7B 
         displayContent: Math.random() > 0.5
      %7D
   %7D
%7D
&lt;/script&gt;

Beispielhafte Ursachen

Client & Server Indikatoren

Inhalte werden abhängig von client- oder serverseitig existierenden Objekten, Variablen oder Werten erzeugt.

Zufall, Ort oder Zeit / Datum

Inhalte werden per Zufall (Math.random()) oder abhängig von Ort (navigator.geolocation) oder Zeit (new Date()) dargestellt und ergeben client- und serverseitig ausgeführt eine unterschiedliche HTML-Struktur.

Nach dem mounted-lifecycle kann auf diese Werte sicher zugegriffen werden, da diese dann nur im client und nach erfolgter Hydration aufgerufen werden.

Authentifizierung

Im SSR on-demand wird häufig auf die Darstellung von user-spezifischen Inhalten verzichtet, um das Caching zu vereinfachen. Bei der static site generation besteht überhaupt kein Zugriff.

Wird im Client aber vor der Hydration user-spezifischer Content auf Grundlage von Cookies, LocalStorage, Login o.ä. gerendert, kann dies zu Hydration-Fehlern führen.

Nach dem mounted-lifecycle kann auf diese Werte sicher zugegriffen werden, da diese dann nur im client und nach erfolgter Hydration aufgerufen werden.

Hash-URLs und Query

Es gibt verschiedene Szenarien, in denen Bestandteile einer URL im SSR nicht zur Verfügung stehen.

Nach dem mounted-lifecycle kann auf diese Werte sicher zugegriffen werden, da diese dann nur im client und nach erfolgter Hydration aufgerufen werden.

Hash

Sollte entgegen dem Standard der Hash einer URL den Inhalte beeinflussen, dann führt dies zu Hydration Fehlern.

Der Wert des Hashes wird vom Browser nicht an den Server übertragen und steht daher beim SSR nicht zur Verfügung bzw. weicht von dem Wert während der Hydration im Browser ab.

Am Beispiel von vue-router sieht es dann so aus:

&lt;!-- index.vue --&gt;

&lt;template&gt;
   &lt;div&gt;
      &lt;p v-if="$route.hash"&gt;foo&lt;/p&gt;
   &lt;/div&gt;
&lt;/template&gt;

Query

Ein anderer Fall ist die Verwendung der Query im SSR. Bei der static-site-generation bspw. in NuxtJS z.B. ist die query immer leer. Hier ist also der query-content "unsafe" und sollte nicht zum Rendern von Inhalten verwendet werden.

Ebenfalls könnte es auch sein, dass man aufgrund eines bei sich eingesetzten Caching-Modells Teile der Query beim SSR ignorieren möchte (z.B. Tracking-Parameter), um ein robustes Caching zu gewährleisten.

Wenn diese Query aber im Client zum Zeitpunkt der Hydration das HTML beeinflusst, führt das zu Fehlern.

3rd Party Tools und HTML-Optimierungen

Tools wie Optimizely und AB Tasty können den DOM verändern, noch bevor die Hydration abgeschlossen ist. Der Support dieser Tools in Bezug auf die Hydration wird aber stets besser.

Aber auch Tools wie HTMLMinifier oder Optimierer bei Cloudflare wie Rocket Loader und AutoMinify können das serverseitig erzeugte HTML so optimieren verändern, dass dies zu Hydration-Fehlern führen kann.

State

Sollte der Inhalt eines State (z.B. vuex, vue-apollo, vue-states) für die HTML-Struktur von Inhalten verantwortlich sein, kann dies zu Hydration-Fehlern führen.

Aus diesem Grund muss der im SSR erzeugte State mit an den Browsern gesendet werden, damit während der Hydration dieselben Daten vorliegen. Sollte der State, der sich im HTML abbildet, zwischen Client und Server abweichen, führt dies zu Hydration-Fehlern.

Wie lassen sich Hydration-Fehler durch unterschiedliche Zustände beheben?

TheAlexLichter ist in einem empfehlenswerten Blog-Post [EN] zu Hydration-Fehlern darauf eingegangen, wie widersprüchliche Zustände aufgelöst werden können.

Die meisten Probleme lassen sich lösen, indem

  • Der Zustand vom Server übertragen und im Client geladen wird, wofür vuex, vue-apollo und vue-states spezielle APIs anbieten,
  • Berechnungen im Client verzögert in dem sicheren mounted Lifecycle Hook durchgeführt werden, oder
  • Notfalls Teile des DOMs ausschließlich im Client gerendert werden, wofür NuxtJS sogar eine eigene Komponente anbietet.

Wie kann ich Hydration-Fehler lokalisieren?

Auch das Lokalisieren von Hydration-Fehlern hat TheAlexLichter in oben genanntem Blog-Post behandelt.
Damit lässt sich vor allem in Entwicklungsumgebungen die Ursache von Hydration-Fehlern finden.

In Produktionsumgebungen hilft manchmal als letztes Mittel das manuelle Vergleichen der mit und ohne JavaScript erzeugten HTML-Struktur.
Durch das Deaktivieren von JavaScript und das anschließende Kopieren des DOMs lassen sich im Gegensatz zum Einsatz von bspw. cURL auch Fehler durch ungültiges HTML finden, die durch den Browser automatisch korrigiert werden. Im Vergleich des DOM kann man dann beispielsweise ein "tbody" finden, das im mit JavaScript erzeugten DOM fehlt.

Wie kann ich Hydration-Fehler automatisch überwachen?

Derzeit entwickeln wir intern ein Tool, um unsere Websites mit Puppeteer regelmäßig in der CI auf Hydration-Fehler zu überprüfen. Sobald dies fertiggestellt ist, werden wir es auf unserem GitHub Profil veröffentlichen.

Bonus

Wir haben hier noch eine kleine Challenge veröffentlicht, wo man das gelernte Wissen anwenden kann, um Hydration-Fehler zu beheben.