Server Side Rendering
Angular Server Side Rendering permite la ejecución de Angular en el servidor para renderizar el HTML antes de devolverle la aplicacion (HTML, JavaScript, CSS) al cliente (navegador). Esto tiene ciertas ventajas:
- Rendimiento a la hora de mostrar la primera página, es decir, el navegador puede renderizar el contenido (ya que lo recibe dentro del index.html) antes de descargar y ejectuar los archivos JavaScript, y por lo tanto Angular. De cara al SEO, esto es muy positivo ya que mejora el LCP ( Largest Contentful Paint).
- Los motores de búsqueda que no son capaces de ejecutar Angular, pueden indexar el contenido del sitio ya que lo genera el servidor. Los enlaces de la página se comportan como enlaces normales que piden al servidor la nueva página. También sirve para que puedan visitar la web navegadores con JavaScript deshabilitado, aunque con funcionalidad limitada.
Una vez se descarga el HTML, CSS y JavaScript, si el cliente lo soporta, se ejecuta Angular y este toma el control de la aplicación, por lo que funcionaría como una aplicación Angular normal. En estos casos SSR nos proporciona una manera rápida de cargar el contenido (sobre todo si usamos algún tipo de caché en el servidor) de la primera página que queremos visualizar.
Activar SSR
Cuando creas un proyecto, Angular te da la opción de activar SSR. Si no se activa en este momento, siempre se puede hacer con el siguiente comando:
ng add @angular/ssr (asegúrate de que tienes Angular en la última versión con ng update)
Si estás en Angular versión 19 te preguntará si quieres habilitar la opción Server Routing para la aplicación. Esta característica nueva permite indicarle a Angular en función de la ruta como se renderizará (en el servidor o en el cliente), además de poder devolver códigos de respuesta HTTP personalizados en cada ruta. La vamos a activar y veremos algunas de sus características.

Por ahora configuraremos que todas las rutas se rendericen de forma dinámica por el servidor:
Server routes
Para renderizar las rutas en el servidor, tenemos que tener en cuenta varias cosas como si el contenido es estático (no va a cambiar) para prerrenderizarlas, o si es una página de error o redirección para devolver los códigos de respuesta/encabezados necesarios. Estas opciones son importantes ya que estamos hablando de una renderización de una página en el lado del servidor, y para los motores de búsqueda y navegadores es información importante.
Si tenemos una página que sabemos que no va a ser nunca indexada por un buscador o que no vamos a cachear en el servidor (una página privada de un usuario por ejemplo) y no nos importa que tarde un poco más en renderizarse en el lado del cliente (liberando de esa carga a nuestro servidor), siempre la podemos poner en modo RenderMode.Client.
Qué no se puede hacer en el servidor
En el lado del servidor no podemos acceder a elementos o APIs específicas del navegador como pueden ser window, document, navigator, location, localStorage, etc. Angular provee algunos servicios de abstraccion como pueden ser DOCUMENT o Location, que podemos inyectar y usar en lugar de los nativos. De esta manera en el servidor en lugar de provocar un error, funcionarán internamente de otra manera. Angular SSR utiliza domino en el servidor para crear una representación interna del DOM.
Para controlar las variaciones de código entre cliente y servidor, podemos inyectar PLATFORM_ID que nos permite saber en combinación con la función isPlatformBrowser si el código se está ejecutando en servidor o en cliente. Por ejemplo, la API de LocalStorage no estará disponible en servidor.
Otra opción para ejecutar código específico de cliente, sobre todo aquel que tiene que ver con manipulación del DOM que solo funciona en el navegador es utilizar las funciones afterRender o afterNextRender. Lo que pongamos ahí solo se ejecutará después de renderizar el DOM en el navegador (cliente). Sería similar a ngAfterViewInit pero en este caso, no se ejecuta en el servidor.
HttpClient con API fetch
Por temas de rendimiento en el servidor, en aplicaciones con Angular SSR activado, se recomienda habilitar que el servicio HttpClient utilice la API Fetch (disponible en NodeJS a partir de la versión 18). Por defecto se utiliza el clásico XMLHttpRequest. Para habilitarlo le pasamos como opción withFetch() a la llamada a provideHttpClient en app.config.ts:
Gestionar páginas con autenticación
Podríamos simplemente no gestionar correctamente la autenticación en el servidor como acabamos de ver en el interceptor un poco más arriba, ya que no tenemos acceso a la API de LocalStorage en este contexto. Aparentemente la aplicación funcionaría correctamente ya que en el servidor, al no tener token, la lógica de nuestra aplicación nos enviaría a la página de login, y esta es la que se renderizaría al cliente.
Sin embargo, en el cliente, una vez Angular toma el control, se vuelven a comprobar los guards, etc. Por lo que inmediatamente habría una redirección a la página correcta donde queríamos ir en un principio. Esto produce un efecto de parpadeo o flickering donde durante unos milisegundos se visualiza la página de login, para inmediatamente pasar a la página correcta.
Uso de cookies
Al ejecutarse el servidor donde tengamos la aplicación Angular SSR en el dominio de la aplicación cliente, podemos utilizar cookies. Cosa que puede no ocurrir con la API de servicios web, que puede estar ejecutándose en un dominio diferente al de la aplicación cliente.
La librería ngx-cookie-service nos permite crear y acceder a las cookies de forma transparente, independientemente de si la aplicación se está ejecutando en el servidor o en el navegador. La instalamos con el siguiente comando:
npm i ngx-cookie-service
En la aplicación simplemente debemos inyectar el servicio CookieService y ya podemos crear y leer cookies en el lado del cliente.
De esta manera, si tenemos un token guardado, el servidor rederizará la página privada en lugar de la página de login. Al ejecutarse Angular en el cliente seguirá en la misma página y no habrá ningún efecto extraño.
Leer cookies desde el servidor
Vamos a crear un servicio llamado SsrCookieService donde tendremos un método para leer cookies en el servidor (SSR). Para ello usaremos el objeto REQUEST de @angular/core que tiene los datos de la petición recibida
ng g s shared/services/ssr-cookie
Después, solo tendremos que utilizar esta funcionalidad cuando queramos obtener el valor del token de autenticación, ya estemos renderizando en el servidor (SSR) o en el cliente.
Hidratación (hydration)
La hidratación o hydration es un proceso mediante el cual la aplicación una vez se ejecuta en el lado del cliente (navegador), es capaz de reutilizar los datos obtenidos en el servidor (http), así como las estructuras DOM creadas por el mismo, en lugar de volver a recrearlas otra vez. Esto implica un mejor rendimiento percibido por el usuario, sobre todo en la interacción inicial con la aplicación, mejorando mucho la métrica conocida como First Input Delay (FID).
Por defecto, al activar SSR en la aplicación, se activa también la hidratación mediante la llamada a provideClientHydration en el array providers de app.config.ts.
Importante: Se deben tener en cuenta la siguientes reglas si queremos que este proceso funcione correctamente y Angular pueda aprovechar la estructura HTML renderizada en el servidor en lugar de recrearla de cero:
- Se debe generar exactamente la misma estructura DOM en el servidor y en el cliente. No se pueden hacer distinciones y generar algo diferente en el servidor.
- No puede haber un proceso o servicio intermedio que altere el HTML entre el servidor y el cliente.
- Las plantillas de los componentes deben de tener una estructura HTML válida. No se puede tener por ejemplo, una tabla sin tbody, un div dentro de un p, un enlace dentro de un h1, un enlace dentro de otro, etc.
ngSkipHydration
Otra cosa que debemos evitar es la manipulación directa del DOM, es decir, acceder directamente al document, crear nodos, modificar o borrar elementos, etc. Angular no detectará estos cambios en el proceso de hidratación e interpretará que el DOM generado no coincide. Ejemplo de error: Hydration Node Mismatch.
Si tenemos componentes que manipulan directamente el DOM, por ejemplo de una librería, podemos indicar a Angular que no los tenga en cuenta para la hidratación usando la directiva ngSkipHydration.
Esta directiva solo puede usarse en componentes de Angular, y no en elementos HTML estándar. Los componentes que tengan esta directiva volverán a renderizarse desde cero en el cliente perdiendo los beneficios de la hidratación (solo para estos componentes).
Carga diferida con @defer
Además de utilizar lazy loading en las rutas, otra forma de optimizar la carga inicial de nuestra aplicación, podemos también diferir la carga de ciertos componentes que aparezcan en la plantilla. Por ejemplo, componentes con alta carga de código, utilizando el bloque @defer.
Esto hará que el código de los componentes, directivas, pipes, que se encuentren dentro de dicho bloque (y no aparezcan fuera del mismo en ningún momento, se empaquete en un archivo aparte que se cargará cuando lo decidamos nosotros.
Por defecto, el código de los componentes, directivas y pipes dentro del bloque se cargará (y se renderizará) cuando el navegador termine de renderizar el resto de la plantilla (idle).
@placeholder
Por defecto no se renderiza nada hasta que Angular cargue el código asociado al bloque @defer. Podemos cambiar ese comportamiento incluyendo un bloque @placeholder asociado, que Angular renderizará en su lugar, hasta que esté listo para renderizar el contenido principal.
El bloque @placeholder acepta por parámetro el tiempo mínimo que se debe mostrar antes de renderizar el contenido principal (aunque este esté disponible antes). Esto es para evitar un efecto molesto para el usuario llamado fast flickering, o parpadeo (sobre todo si el contenido se renderiza en la parte que está visualizando el usuario en ese momento).
@loading
Se puede incluir también un bloque @loading. Este se mostrará cuando el código del bloque empieza a cargarse, sustituyendo al bloque @placeholder (si está presente) y se ocultará cuando el bloque principal @defer esté cargado.
Este bloque acepta opciones similares a @placeholder:
- minimum - El tiempo mínimo que se mostrará (aunque el bloque @defer esté cargado)
- after - El tiempo que espera desde que empieza a cargar el componente hasta que muestra el contenido del bloque @loading.
@error
También se puede incluir un bloque @error para mostrar cuando falle la carga del bloque @defer. Por ejemplo, si la conexión a internet no es buena y a veces falla.
Triggers para @defer
Se pueden añadir opciones al bloque @defer llamados triggers. Estas opciones determinarán cuando comenzará la carga del código asociado a dicho bloque y son las siguientes:
- on idle: Esta es la opción por defecto. Se carga cuando el navegador termina de renderizar la aplicación.
- on viewport: Se carga cuando el contenido del bloque asociado @placeholder o de un elemento referenciado entra en la vista del usuario.
- on interaction: Se carga cuando el usuario interactúa con el contenido de @placeholder o un elemento referenciado (click, keydown).
- on hover: Se carga cuando el usuario pasa el ratón por encima del elemento dentro de @placeholder o del elemento referenciado (también si el elemento obtiene el foco).
- on inmediate: Carga lo antes posible, tan pronto como el resto del código del componente ha sido cargado.
- on timer: Carga después de una cantidad de tiempo establecida
- when: Carga cuando se cumple una condición (booleano)
Precarga con prefetch
Además de las opciones de carga, también se puede establecer una opción de precarga (prefetch) en el bloque @defer. De esta manera podemos indicarle que empiece a cargar antes el código asociado al bloque, de tal manera que cuando se cumpla la condición, el código ya esté cargado y solo tenga que renderizarlo.
@defer y SSR
Los bloques @defer no se renderizan en el servidor. En su lugar se renderizará el bloque @placeholder asociado. Si este tiene un tamaño similar al fragmento renderizado evitaríamos el efecto cumulative layout shift (CLS) que es una métrica importante a la hora de medir la experiencia de usuario en la aplicación.
SIn embargo, a partir de la versión 19, Angular incluye la posibilidad de renderizar este bloque en el servidor e hidratarlo en diferido utilizando Incremental Hydration, que veremos a continuación.
Hidratación incremental con @defer
Desde la versión 19 (en fase developer preview) se introduce la posibilidad de renderizar el contenido de un bloque @defer en el servidor (permitiendo que los buscadores lo indexen). Además, se siguen manteniendo las ventajas de usar @defer ya que en el cliente, el componente se mantiene como contenido estático y no es interactivo (se carga el código asociado) hasta que no se cumpla una determinada condición.
Para habilitar esta característica añadimos la opción withIncrementalHydration a la función provideClientHydration en el archivo app.config.ts. Esta opción incluye la funcionalidad de withEventReplay, por lo que podemos quitarlo sin problema.
En los bloques @defer podemos definir nuevos triggers de tipo hydrate. Cuando un trigger de este tipo está presente, Angular renderiza el bloque en el servidor y por tanto se renderiza en el cliente junto al resto del contenido desde el primer momento. Sin embargo, el código, y por tanto la funcionalidad, asociado a dicho bloque no se cargará en el cliente hasta que no ocurra el evento que pongamos en el trigger.
Cuando el componente viene renderizado del servidor, ya no se mostrará el bloque @placeholder ni @loading en el cliente, sino que se mostrará el contenido del bloque @defer, pero estático. Sin embargo, una vez cargado Angular en el cliente (si este es un navegador normal con soporte JavaScript), el resto de la navegación ocurre en el lado del cliente con el comportamiento que vismo anteriormente.
- hydrate on idle - La carga ocurre cuando el navegador termina la carga y renderizado de toda la aplicación.
- hydrate on viewport - Cuando el contenido entra en la vista
- hydrate on interaction - Cuando el usuario interactúa (click)
- hydrate on hover - Cuando el ratón entra en el área del bloque
- hydrate on immediate - Inmediatamente después de que el resto del contenido (no diferido) de la plantilla se renderice
- hydrate on timer - Después de un tiempo prestablecido.
- hydrate when condición - Cuando la condición o booleano sea true
- hydrate never - El contenido se quedará estático y no será interactivo
Se pueden combinar los triggers de servidor (hydrate) con los de cliente. En el caso de que la página venga renderizada desde el servidor (primera carga o navegador sin JS), se aplicará el trigger hydrate, mientras que una vez tome el control Angular y la navegación y renderizado ocurra en el cliente, se utilizará el otro trigger (y también se renderizará el bloque @placeholder, por ejemplo).
Tanto en la hidratación normal, como en la incremental, si se producen eventos de interacción antes de que esté cargada la funcionalidad en el cliente y el componente sea interactivo, Angular guardará esos eventos y los volverá a reproducir una vez el componente sea funcional. Esta característica se llama Event Replay y al habilitar la hidratación incremental está disponbile.