Intenté migrar una aplicación de NextJS en producción desde el pesado runtime standalone de Node.js a una arquitectura ligera. El objetivo era utilizar output: 'export' de NextJS para generar activos estáticos, que después serían servidos por un servidor en Go personalizado y ligero, empaquetado en la misma imagen de Docker.
La aplicación era un sitio web complejo e internacionalizado que admitía ocho configuraciones regionales distintas. Un requisito principal era mantener un build universal. Esto implicaba que la imagen de Docker debía ser agnóstica respecto a las variables de entorno para permitir el despliegue en varias etapas sin recompilar. El objetivo principal era eliminar el consumo de RAM del pesado runtime de NextJS. El runtime NodeJS/Bun consumía ~ 200-450MB de RAM en el modo standalone y ~140MB en la exportación estática.
Para lograr el build universal, se eliminaron las variables de entorno del proceso de compilación. En lugar de incrustar endpoints de API en el frontend estático, se utilizaron rutas estáticas como /api/graphql. La responsabilidad de manejar estas solicitudes se asignó al servidor de Go incluido, que actuaba como proxy para reenviar el tráfico a los servicios de backend apropiados definidos por las variables de entorno en tiempo de ejecución.
La optimización de imágenes también se desacopló del runtime de NextJS. Se implementó un cargador de imágenes personalizado para reescribir las solicitudes de imágenes hacia una ruta de CDN dedicada, garantizando que la exportación estática permaneciera independiente de la lógica de procesamiento de imágenes.
Surgió un obstáculo importante en el manejo de rutas dinámicas, como las entradas del blog a las que se accede mediante un slug. Las exportaciones estáticas de NextJS requieren generateStaticParams para definir todas las rutas en tiempo de compilación. Para evitar la ineficiencia de pre-renderizar todas las entradas posibles, se modificó la arquitectura para utilizar parámetros de consulta en lugar de segmentos de ruta dinámicos. Esto permitió servir un único archivo HTML estático genérico para todas las solicitudes de contenido, que la aplicación del lado del cliente luego rellenaría con datos específicos.
El proyecto se abandonó finalmente por un conflicto irresoluble relacionado con el posicionamiento en buscadores (SEO), concretamente los requisitos de los metadatos de la URL canónica.
Aunque la mayor parte de los metadatos puede permanecer estática sin un impacto significativo, la etiqueta canonical requiere una URL absoluta, incluido el dominio completo, para funcionar correctamente. En el directorio app de NextJS, los metadatos deben definirse mediante generateMetadata o la exportación estática metadata, ambas se ejecutan exclusivamente en tiempo de compilación para las exportaciones estáticas. No hay un mecanismo incorporado para posponer esto a tiempo de ejecución sin un servidor de Node.js. En consecuencia, en una exportación estática universal, el nombre de dominio no puede conocerse en tiempo de compilación.
Para rectificarlo dentro de la arquitectura propuesta, el servidor en Go tendría que analizar el HTML estático y reemplazar dinámicamente el dominio o un valor de marcador de posición similar a un dominio dentro de la cadena de la etiqueta canonical en tiempo de ejecución. Se rechazó este enfoque porque, en la práctica, exige implementar plantillas basadas en Go sobre una exportación estática. Tal manipulación no es elegante y introduce un riesgo significativo de problemas de hidratación, en los que la aplicación React del lado del cliente no coincide con el HTML modificado enviado por el servidor.
A menos que necesites posicionamiento SEO, es viable un runtime ligero con una imagen de Docker multi-entorno. En caso contrario, solo puede lograrse mediante soluciones muy hacky, como sustituir las variables ENV de marcador de posición dentro de los .html y .js estáticos mediante un motor de plantillas personalizado.
Para las apps de NextJS en las que necesitas variables de entorno dinámicas como BACKEND_URL, API_ENDPOINT o una reescritura personalizada, puedes usar un servidor ligero personalizado basado en Golang. Es sencillo y funciona (~13 MB de uso de RAM). Hay muchas soluciones alternativas que se pueden implementar para cubrir el abanico de funcionalidades.
Para un sitio web donde el SEO no es importante, hay muchos servidores que pueden alojar tu exportación estática. Por ejemplo, en uno de mis proyectos uso un static-web-server basado en Rust (~8 MB de uso de RAM).
Así quedó el intento en cifras:
Si hubiera querido lograr el resultado deseado a toda costa, la actualización habría requerido aproximadamente 4× más líneas insertadas y cambios en ~100 archivos.
