Server-Side Rendering en Angular: SSR + Hydration para máximo rendimiento
El Server-Side Rendering (SSR) ha sido durante mucho tiempo una debilidad de Angular comparado con frameworks como Next.js. Pero con Angular 17+, el panorama ha cambiado radicalmente gracias a la hidratación no destructiva y el nuevo sistema de SSR integrado.
¿Por qué necesitas SSR?
Sin SSR, una aplicación Angular funciona así:
- El navegador descarga un HTML vacío (
<app-root></app-root>). - Descarga y ejecuta todo el JavaScript.
- Angular renderiza la aplicación.
- El usuario finalmente ve contenido.
Esto genera:
- Mal SEO: Los crawlers ven una página vacía.
- CLS alto: El contenido "salta" al renderizarse.
- LCP lento: El primer contenido visible tarda en aparecer.
Con SSR:
- El servidor genera el HTML completo.
- El navegador lo muestra inmediatamente (LCP rápido).
- Angular se "hidrata" sobre el HTML existente.
- La app se vuelve interactiva.
Configuración en Angular 17+
Crear un proyecto con SSR es tan sencillo como:
ng new mi-proyecto --ssr
O añadir SSR a un proyecto existente:
ng add @angular/ssr
Esto genera automáticamente:
server.ts: El servidor Express.app.config.server.ts: Configuración del servidor.- Actualización de
angular.jsoncon targets de SSR.
Configuración del servidor
// app.config.server.ts
import { mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig = {
providers: [
provideServerRendering(),
provideServerRouting(serverRoutes)
]
};
export default mergeApplicationConfig(appConfig, serverConfig);
Hidratación No Destructiva
Este es el cambio más importante. En versiones anteriores, Angular destruía el HTML del servidor y lo reconstruía desde cero. Ahora, Angular reutiliza el DOM existente.
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig = {
providers: [
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideHttpClient(withFetch()),
]
};
¿Qué hace withEventReplay()?
Captura los eventos del usuario (clics, inputs) que ocurren antes de que Angular termine de hidratarse, y los reproduce después. Así no se pierden interacciones tempranas.
Verificar que la hidratación funciona
En las DevTools de Chrome, busca en la consola:
Angular hydrated X component(s) and X node(s), X component(s) were skipped.
Si ves "the entire application was destroyed and re-rendered", hay un problema de mismatch.
Rutas del Servidor: Control fino
Puedes controlar qué rutas se renderizan en el servidor, se pre-renderizan (SSG) o se dejan solo para el cliente:
// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
renderMode: RenderMode.Prerender // SSG: genera HTML en build
},
{
path: 'blog/:slug',
renderMode: RenderMode.Server // SSR: genera HTML por request
},
{
path: 'dashboard/**',
renderMode: RenderMode.Client // SPA: solo cliente
},
{
path: '**',
renderMode: RenderMode.Server
}
];
¿Cuándo usar cada modo?
- Prerender (SSG): Páginas que no cambian frecuentemente (landing, about, blog posts).
- Server (SSR): Contenido dinámico que necesita SEO (perfiles de usuario, búsquedas).
- Client (SPA): Dashboards privados que no necesitan SEO.
Manejo de APIs del navegador en SSR
Un error clásico es usar window, document o localStorage en código que se ejecuta en el servidor:
// ❌ Esto rompe en SSR
@Component({...})
export class BadComponent {
width = window.innerWidth; // ERROR: window is not defined
}
// ✅ Solución con isPlatformBrowser
import { isPlatformBrowser, PLATFORM_ID } from '@angular/common';
@Component({...})
export class GoodComponent {
private platformId = inject(PLATFORM_ID);
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
// Seguro usar APIs del navegador aquí
const width = window.innerWidth;
}
}
}
// ✅ Solución moderna con afterNextRender
@Component({...})
export class ModernComponent {
constructor() {
afterNextRender(() => {
// Solo se ejecuta en el navegador, después del primer render
const observer = new IntersectionObserver(...);
});
}
}
Transfer State: Evitar doble fetch
Sin configuración adicional, las peticiones HTTP se ejecutarían tanto en servidor como en cliente. El TransferState serializa las respuestas del servidor y las reutiliza en el cliente:
// Esto ya está incluido automáticamente con provideClientHydration()
// y provideHttpClient(withFetch())
// Angular automáticamente cachea las peticiones HTTP hechas en SSR
// y las reutiliza en el cliente. No necesitas código adicional.
Conclusión
SSR + Hydration en Angular moderno es una combinación poderosa que cierra la brecha con Next.js y Nuxt. Con provideClientHydration, rutas de servidor y pre-renderizado, puedes conseguir puntuaciones de Lighthouse cercanas a 100 sin sacrificar la experiencia de desarrollo de Angular.