In einem Web-Projekt habe ich kürzlich mit einem Fehler gerungen, der mich fast eine Woche lang beschäftigt hat. Und als ich die Ursache schließlich gefunden hatte, konnte ich den Fehler beheben, ohne eine einzige Zeile Code zu schreiben. Davon möchte ich berichten.
Der Fehler trat in einer Nuxt Single-Page-Application auf und äußerte sich so, dass die Website kurz nach dem initialen Laden ein weiteres Mal gerendert wurde. Dabei wurden alle UI-Elemente ersetzt, was insbesondere bedeutete, dass jegliche Eingaben die der Nutzer bereits getätigt hatte, wieder verschwunden sind. Der Kunde hatte mir ein Video von dem Fehler geschickt und der Fehler war als deutliches Flackern der Seite zu erkennen.
Leider konnte ich das allerdings in keinem Webbrowser reproduzieren. Die Aussage des Kunden war, dass der Fehler nur morgens auftreten würde, nachdem er die Seite eine Weile nicht mehr besucht hatte. Mein erster Gedanke war natürlich, dass das Problem dann in irgendeiner Form mit dem Cache zu tun haben müsse. Aber trotz deaktiviertem Browser-Cache konnte ich den Fehler zunächst nicht nachstellen – auch nicht um 6 Uhr morgens. Jeder Softwareentwickler weiß: die fiesesten Bugs sind die, die sich nicht verlässlich reproduzieren lassen!
Mithilfe der Chrome Developer-Tools habe ich daraufhin einen Breakpoint auf das Script First Statement Event gesetzt, um die Seite Schritt für Schritt zu laden und zu schauen, ob mir dabei etwas auffällt. Und tatsächlich: Damit konnte ich den Fehler in der Produktionsumgebung reproduzieren. Initial wurde die Seite vollständig geladen und angezeigt. Nachdem das JavaScript der Nuxt-App geladen wurde, verschwanden allerdings einige UI-Elemente beim nächsten Breakpoint wieder und wurden beim darauffolgenden Breakpoint erneut gerendert
Der Fehler schien bei mir also die ganze Zeit aufgetreten zu sein, allerdings so schnell, dass man ihn mit bloßem Auge gar nicht bemerken konnte. Lokal oder in der Staging-Umgebung trat der Fehler bei mir allerdings weiterhin nicht auf. Wieder war ich schnell mit meinen Rückschlüssen und war sicher, dass dann eine Diskrepanz in der Konfiguration, insbesondere der Umgebungsvariablen, der Grund sein müsse. Und wieder lag ich falsch: selbst mit nahezu identischen Umgebungsvariablen trat das Problem weiterhin nur in Produktion auf.
Weil ich ziemlich im Dunkeln tappte, holte ich mir Hilfe bei meinem Kollegen Jan, der sich mit Nuxt ohnehin etwas besser auskennt als ich. Und Jan konnte mich zumindest in die richtige Richtung schubsen, indem er mir Nuxt’s Re-Hydration erklärte.
Nuxt ist ein Frontend JavaScript Framework, das auf Vue aufbaut. Anders als Vue, das ausschließlich client-seitiges Rendering unterstützt, bietet Nuxt die Option zum server-seitigen Rendern der Seite. Grob gesagt funktioniert das so: Nuxt rendert die Vue-App initial auf dem Server und schickt das statische HTML an den Client. Der Client rendert das HTML, lädt daraufhin den JavaScript Code nach und injiziert diesen dann in das DOM, wodurch die Vue-Komponenten gemounted werden und die Seite ihre dynamische Funktionalität erhält. Dieser Prozess nennt sich “Hydration”. Der Nutzer bekommt davon nichts mit, außer es läuft etwas schief.
Um sicherzustellen, dass die Seite korrekt funktioniert, prüft Nuxt ob das durch die Hydration entstehende HTML identisch zu dem statischen HTML ist, das der Server initial gesendet hat. Ist es das nicht, rendert Nuxt die Seite nochmal neu, sodass alle Komponenten erneut gemounted werden.
Es gibt unterschiedliche Gründe, aus denen es zur Re-Hydration kommen kann. Hier ein paar Beispiele:
- Ungültiges HTML, beispielsweise ein
<div>
, das in einem <p>
verschachtelt ist. - Ein v-if das auf dem Server anders evaluiert als auf dem Client.–
- Dynamischer content der über v-html eingebunden wird und auf dem Client einen anderen Inhalt hat als auf dem Server.
In jedem Fall sah es so aus, als wäre genau das, also die Re-Hydration, das Problem. Was mir noch nicht klar war: warum passiert das und wieso passiert es nur in Produktion?
Theoretisch hätten alle drei oben genannten Gründe die Ursache dafür sein können. Die Nuxt-App holte einige HTML-Inhalte aus einem CMS. An keiner Stelle wurde geprüft ob diese Inhalte HTML5-konform sind. Sie hätten also durchaus ungültiges HTML enthalten und für die Re-Hydration sorgen können. Allerdings nutzte die Staging-Umgebung dieselben CMS-Inhalte, insofern konnte ich das als Ursache ebenfalls ausschließen.
Natürlich gab es auch viele v-if Anweisungen, aber da das Problem selbst mit identischen Umgebungsvariablen nur in Produktion auftrat, konnte auch das nicht der Grund für das Problem sein.
Schließlich ging mir ein Licht auf: der einzig verbleibende Unterschied zwischen der Staging- und Produktionsumgebung war, dass letztere von Cloudflare – einem CDN – gecached und geserved wurde. Meinen Browser-Cache zu deaktivieren hatte natürlich keinerlei Auswirkungen auf den Cache von Cloudflare. Könnte es also sein, dass Cloudflare mir “altes” HTML schickt und sich Nuxt daran während der Hydration stört?
Ich probierte den Cloudflare Cache zu löschen und sogar komplett zu deaktivieren, aber es machte keinen Unterschied — die Re-Hydration trat weiterhin auf. Ich wusste, dass ich auf dem richtigen Weg bin und trotzdem hatte ich keine weitere Idee.
Um auszuschließen, dass in Produktion nicht doch vielleicht ein zusätzliches Skript geladen wurde, dass in den anderen Umgebungen nicht existierte, nutzte ich noch mal den Script First Statement Breakpoint und schaute parallel durch jedes einzelne Skript, dass auf Staging und in Produktion geladen wurde. Und dann fiel es mir auf: in Produktion fehlten im serverseitig gerenderten HTML die leeren Kommentare, die Vue einfügt, wenn Elemente aufgrund von Conditional-Rendering ausgeblendet werden. Erst nachdem die App clientseitig gemounted wurde, tauchten die Kommentare wieder auf. Vue braucht diese Kommentare, um die ausgeblendeten Elemente ggf. später wieder einzublenden. Und die Vue-Dokumentation sagt sogar explizit, dass auch Kommentare Nodes sind.
Every element is a node. Every piece of text is a node. Even comments are nodes!
Somit können natürlich auch fehlende Kommentare zur Re-Hydration führen. Und warum verschwinden die Kommentare? Weil Cloudflare den Code noch einmal minifiziert, bevor er an den Client geschickt wird — und natürlich weiß Cloudflare nicht, dass die Kommentare für eine Nuxt-App relevant sind und entfernt sie einfach. Es war also ganz einfach: ich musste nur das “Auto-Minify” Feature von Cloudflare deaktivieren und das Problem war verschwunden.
Auch wenn am Ende nur ein Knopfdruck nötig war hat mich das Problem insgesamt fast eine Woche lang beschäftigt. Ohne den entscheidenden Hinweis von meinem Kollegen Jan hätte es vermutlich noch länger gedauert und ohne die Chrome Dev-Tools wäre ich vielleicht sogar nie ans Ziel gekommen. Außerdem war es wichtig, das Problem reproduzierbar zu machen, damit ich die Ursache systematisch eingrenzen konnte.
Das sind drei entscheidende Faktoren, die dabei helfen Fehler effizient zu beheben:
- Mach das Problem reproduzierbar, dann grenze die Ursache systematisch ein.
- Kenne und nutze deine Tools: den Code-Editor, den Debugger, Monitoring-Services, die Developer-Tools usw.
- Und am wichtigsten: hol dir Hilfe, wenn du nicht weiterweißt.
Es gibt aber noch einen vierten, entscheidenden Punkt: den Fehler reflektieren. Das umfasst zum einen, zu erkennen warum der Fehler aufgetreten ist und zum anderen, dafür zu sorgen, dass er nicht wieder auftritt — weder in diesem, noch in einem anderen Projekt. In erster Linie heißt das, die gewonnenen Informationen mit seinen Teamkollegen zu teilen. Im zweiten Schritt sollte man die Informationen in einer README oder einem Wiki festhalten, sodass sie auch zukünftig nicht in Vergessenheit geraten.
Bei bitside sind diese Dinge für uns selbstverständlich. Obwohl wir alle in unterschiedlichen Projekten arbeiten, gibt es einen engen Austausch unter Kollegen — fachlich, aber auch privat. Wir profitieren vom heterogenen Wissen im Team und bilden uns in regelmäßigen Trainings gemeinsam weiter. Wöchentlich treffen wir uns zu Coding-Challenges oder testen bei internen Projekten gemeinsam die neuesten Frameworks und Technologien.