Ich habe versucht, eine produktive NextJS-Anwendung von der schweren Node.js-standalone-Runtime auf eine schlanke Architektur zu migrieren. Das Ziel war, NextJS output: 'export' zu nutzen, um statische Assets zu erzeugen, die dann von einem benutzerdefinierten, leichtgewichtigen Go-Server bedient werden, der im selben Docker-Image gebündelt ist.
Die Anwendung war eine komplexe, internationalisierte Website, die acht unterschiedliche Locales unterstützte. Eine zentrale Anforderung war die Beibehaltung eines universellen Builds. Das bedeutete, dass das Docker-Image gegenüber Umgebungsvariablen agnostisch sein musste, um Deployments in verschiedenen Stufen ohne Neuaufbau zu ermöglichen. Das Hauptziel war, den RAM-Verbrauch der schweren NextJS-Runtime loszuwerden. Die NodeJS/Bun-Runtime verbrauchte ~ 200-450MB RAM für den standalone-Modus und ~140MB für den statischen Export.
Um den universellen Build zu erreichen, wurden Umgebungsvariablen aus dem Build-Prozess entfernt. Anstatt API-Endpunkte in das statische Frontend einzubrennen, wurden statische Pfade wie /api/graphql verwendet. Die Verantwortung für die Verarbeitung dieser Anfragen wurde dem gebündelten Go-Server übertragen, der als Proxy fungierte und den Traffic an die passenden Backend-Services weiterleitete, die zur Laufzeit über Umgebungsvariablen definiert wurden.
Die Bildoptimierung wurde ähnlich von der NextJS-Runtime entkoppelt. Ein benutzerdefinierter Image-Loader wurde implementiert, um Bildanfragen auf einen dedizierten CDN-Pfad umzuschreiben, damit der statische Export unabhängig von der Bildverarbeitungslogik bleibt.
Ein bedeutendes Hindernis ergab sich bei der Behandlung dynamischer Routen, etwa von Blogposts, die über einen Slug aufgerufen werden. NextJS Static Exports erfordern , um alle Pfade zur Build-Zeit zu definieren. Um die Ineffizienz des Vor-Renderns jedes möglichen Blogposts zu vermeiden, wurde die Architektur so geändert, dass Query-Parameter statt dynamischer Routen-Segmente genutzt werden. Dadurch konnte für alle Inhaltsanfragen eine einzige generische statische HTML-Datei ausgeliefert werden, die die clientseitige Anwendung anschließend mit spezifischen Daten befüllte.
generateStaticParamsDas Projekt wurde letztlich aufgrund eines unauflösbaren Konflikts in Bezug auf Suchmaschinenoptimierung aufgegeben, konkret wegen der Anforderungen an die Canonical-URL-Metadaten.
Während die meisten Metadaten ohne große Auswirkungen statisch bleiben können, benötigt das Canonical-Tag eine absolute URL inklusive vollständiger Domain, um korrekt zu funktionieren. Im App-Verzeichnis von NextJS müssen Metadaten über generateMetadata oder den statischen metadata-Export definiert werden; beide werden für statische Exporte ausschließlich zur Build-Zeit ausgeführt. Es gibt keinen eingebauten Mechanismus, dies ohne einen Node.js-Server auf die Laufzeit zu verschieben. Folglich kann bei einem universellen statischen Export der Domainname zur Build-Zeit nicht bekannt sein.
Um dies innerhalb der vorgeschlagenen Architektur zu beheben, müsste der Go-Server das statische HTML parsen und zur Laufzeit die Domain oder einen domainähnlichen Platzhalterwert innerhalb des Canonical-Tag-Strings dynamisch ersetzen. Dieser Ansatz wurde verworfen, da er faktisch die Implementierung eines Go-basierten Templatings über einem statischen Export erfordert. Eine solche Manipulation ist nicht elegant und birgt ein erhebliches Risiko von Hydration-Problemen, bei denen die clientseitige React-Anwendung nicht mit dem modifizierten, vom Server gesendeten HTML übereinstimmt.
Wenn du kein SEO-Ranking benötigst, ist eine leichtgewichtige Runtime mit einem Multi-Env-Docker-Image erreichbar. Andernfalls ist es nur über sehr hackige Lösungen machbar, etwa indem Platzhalter-ENV-Variablen in statischen .html und .js per eigener Templating-Engine ersetzt werden.
Für NextJS-Apps, in denen du dynamische Umgebungsvariablen wie BACKEND_URL, API_ENDPOINT oder ein Custom-Rewrite benötigst, kannst du einen leichtgewichtigen, eigenen Golang-basierten Server verwenden. Es ist einfach, und es funktioniert (~13 MB RAM-Verbrauch). Es gibt viele Workarounds, die für verschiedenste Features implementiert werden können.
Für Websites, bei denen SEO nicht wichtig ist, gibt es viele Server, die deinen statischen Export hosten können. In einem meiner Projekte nutze ich zum Beispiel einen Rust-based static-web-server (~8 MB RAM-Verbrauch).
So sah der Versuch in Zahlen aus:
Wenn ich das gewünschte Ergebnis um jeden Preis erreichen wollte, hätte das Update ungefähr 4× mehr eingefügte Zeilen und Änderungen in ~100 Dateien erfordert.
