Volver al blog
Angular SSR Performance SEO
Imagen de portada del artículo: Server-Side Rendering en Angular: SSR + Hydration para máximo rendimiento

Server-Side Rendering en Angular: SSR + Hydration para máximo rendimiento

14 min
German

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í:

  1. El navegador descarga un HTML vacío (<app-root></app-root>).
  2. Descarga y ejecuta todo el JavaScript.
  3. Angular renderiza la aplicación.
  4. El usuario finalmente ve contenido.

Esto genera:

Con SSR:

  1. El servidor genera el HTML completo.
  2. El navegador lo muestra inmediatamente (LCP rápido).
  3. Angular se "hidrata" sobre el HTML existente.
  4. La app se vuelve interactiva.

Configuración en Angular 17+

Crear un proyecto con SSR es tan sencillo como:

bash
ng new mi-proyecto --ssr

O añadir SSR a un proyecto existente:

bash
ng add @angular/ssr

Esto genera automáticamente:

Configuración del servidor

typescript
// 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.

typescript
// 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:

code
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:

typescript
// 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?

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:

typescript
// ❌ 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:

typescript
// 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.