J'ai tenté de migrer une application NextJS en production du runtime Node.js standalone lourd vers une architecture légère. L'objectif était d'utiliser NextJS output: 'export' pour générer des assets statiques, qui seraient ensuite servis par un serveur Go léger, personnalisé, embarqué dans la même image Docker.
L'application était un site web complexe et internationalisé prenant en charge huit locales distinctes. Une exigence principale était de conserver un build universel. Cela signifiait que l'image Docker devait être agnostique des variables d'environnement afin de permettre le déploiement sur divers environnements sans reconstruire. L'objectif principal était d'éliminer la consommation de RAM du runtime NextJS lourd. Le runtime NodeJS/Bun consommait ~ 200-450MB de RAM en mode standalone et ~140MB pour l'export statique.
Pour obtenir le build universel, les variables d'environnement ont été retirées du processus de build. Au lieu d'intégrer en dur les endpoints d'API dans le frontend statique, des chemins statiques comme /api/graphql ont été utilisés. La responsabilité de traiter ces requêtes a été confiée au serveur Go embarqué, qui jouait le rôle de proxy pour rediriger le trafic vers les services backend adéquats définis par les variables d'environnement au runtime.
L'optimisation des images a été de même découplée du runtime NextJS. Un chargeur d'images personnalisé a été implémenté pour réécrire les requêtes d'images vers un chemin CDN dédié, garantissant que l'export statique reste indépendant de la logique de traitement d'images.
Un obstacle majeur est apparu concernant la gestion des routes dynamiques, comme les articles de blog accessibles via un slug. Les exports statiques de NextJS exigent generateStaticParams pour définir tous les chemins au moment du build. Pour éviter l'inefficacité de pré-rendre chaque article de blog possible, l'architecture a été modifiée pour utiliser des paramètres de requête plutôt que des segments de route dynamiques. Cela a permis de servir un seul fichier HTML statique générique pour toutes les requêtes de contenu, que l'application côté client remplissait ensuite avec des données spécifiques.
Le projet a finalement été abandonné en raison d'un conflit insoluble concernant le SEO, plus précisément les exigences liées aux métadonnées d'URL canonique.
Alors que la plupart des métadonnées peuvent rester statiques sans impact majeur, la balise canonique exige une URL absolue, incluant le domaine complet, pour fonctionner correctement. Dans le répertoire app de NextJS, les métadonnées doivent être définies via generateMetadata ou l'export statique metadata, qui s'exécutent tous deux exclusivement au moment du build pour les exports statiques. Il n'existe aucun mécanisme intégré pour reporter cela à l'exécution sans serveur Node.js. Par conséquent, dans un export statique universel, le nom de domaine ne peut pas être connu au moment du build.
Pour corriger cela dans l'architecture proposée, il faudrait que le serveur Go analyse le HTML statique et remplace dynamiquement le domaine ou une valeur fictive de type domaine dans la chaîne de la balise canonique à l'exécution. Cette approche a été rejetée car elle revient à implémenter un templating en Go par-dessus un export statique. Une telle manipulation n'est pas élégante et introduit un risque important de problèmes d'hydratation, où l'application React côté client n'est plus en phase avec le HTML modifié envoyé par le serveur.
Sauf si vous avez besoin de classement SEO, un runtime léger avec une image Docker multi-env est réalisable. Sinon, cela ne peut être atteint qu'au prix de solutions très bancales, comme remplacer les variables ENV factices à l'intérieur des fichiers statiques .html et .js via un moteur de templating personnalisé.
Pour les applications NextJS où vous avez besoin de variables d'environnement dynamiques comme BACKEND_URL, API_ENDPOINT, ou une réécriture personnalisée, vous pouvez utiliser un serveur léger personnalisé basé sur Golang. C'est simple, et ça marche (~13 MB de RAM). Il existe beaucoup de contournements possibles pour couvrir l'éventail des fonctionnalités.
Pour les sites où le SEO n'est pas important, il existe de nombreux serveurs capables d'héberger votre export statique. Par exemple, dans l'un de mes projets, j'utilise un static-web-server basé sur Rust (~8 MB de RAM).
Voici à quoi ressemblait la tentative en chiffres :
Si je devais atteindre le résultat souhaité à tout prix, la mise à jour aurait nécessité environ 4× plus de lignes insérées et des modifications sur ~100 fichiers.
