Codea Bien Logo
Angular rendering: CSR, SSR y SSG
Angular

Angular rendering: CSR, SSR y SSG

Kevin Dávila

angularrenderingcsrssrssg

Cuando construyes una aplicación Angular, la forma en que el HTML llega al navegador importa tanto como lo que ese HTML contiene. Afecta el SEO, la velocidad percibida, el coste de infraestructura y la experiencia del usuario.

Hoy hablaremos sobre las tres estrategias disponibles — CSR, SSR y SSG — y cómo combinarlas en la misma app. Para que la comparación sea honesta, voy a usar el mismo ejemplo en cada caso: una página de artículos de blog con una lista de posts. (como este Blog, curiosamente)


El ejemplo base

Antes de entrar en las estrategias, el componente que vamos a renderizar de las tres formas es este:

// articles.component.ts
import { Component, inject, OnInit, signal } from '@angular/core';
import { ArticlesService } from './articles.service';

@Component({
  selector: 'app-articles',
  template: `
    <h1>Artículos</h1>
    @for (article of articles(); track article.id) {
      <article>
        <h2>{{ article.title }}</h2>
        <p>{{ article.summary }}</p>
      </article>
    }
  &#96;
})
export class ArticlesComponent implements OnInit {
  private readonly articlesService = inject(ArticlesService);
  readonly articles = signal<Article[]>([]);

  ngOnInit() {
    this.articlesService.getAll().subscribe(data => this.articles.set(data));
  }
}

El componente es el mismo en los tres casos. Lo que cambia es dónde y cuándo se ejecuta.


CSR — Client-Side Rendering

Es el comportamiento por defecto de Angular. No necesitas configurar nada.

El servidor devuelve un HTML casi vacío:

<!-- Lo que el navegador recibe inicialmente -->
<!doctype html>
<html>
  <body>
    <app-root></app-root>
    <script src="main.js"></script>
  </body>
</html>

El navegador descarga main.js, Angular arranca, y el componente se renderiza. Solo en ese momento el usuario ve el contenido.

# Crear un proyecto SPA (comportamiento por defecto)
ng new mi-blog
ng serve

Lo que ocurre con el ejemplo:

  1. El navegador recibe el HTML vacío

  2. Descarga y ejecuta main.js

  3. Angular monta ArticlesComponent

  4. ngOnInit dispara la llamada HTTP a la API

  5. Llegan los datos, Angular renderiza los artículos

El usuario ve un momento de pantalla en blanco o un spinner mientras ocurren los pasos 2 al 5.

Cuándo tiene sentido: paneles de administración, dashboards, apps internas, herramientas que requieren autenticación previa. Para estas apps el SEO no importa y la primera carga puede ser lenta sin penalización real.

Cuándo no: páginas públicas que necesitan indexarse en buscadores o aparecer en previsualizaciones de redes sociales.

Source: Angular — Client-side rendering


SSR — Server-Side Rendering

Con SSR, Angular se ejecuta en el servidor antes de responder al navegador. El servidor devuelve HTML ya construido con el contenido:

<!-- Lo que el navegador recibe con SSR -->
<!doctype html>
<html>
  <body>
    <app-root>
      <h1>Artículos</h1>
      <article>
        <h2>Cómo funciona NgRx SignalStore</h2>
        <p>SignalStore es la nueva forma de gestionar estado...</p>
      </article>
      <article>
        <h2>Angular 19: novedades principales</h2>
        <p>La versión 19 trajo el renderizado híbrido...</p>
      </article>
    </app-root>
    <script src="main.js"></script>
  </body>
</html>

El usuario ve el contenido de inmediato, sin esperar a que JavaScript arranque.

# Añadir SSR a un proyecto existente
ng add @angular/ssr

# O crear un proyecto nuevo con SSR
ng new mi-blog --ssr

Esto genera dos archivos de configuración nuevos:

// app.config.server.ts
import { ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/ssr';

export const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
  ]
};
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      withEventReplay()
    ),
  ]
};

Lo que ocurre con el ejemplo:

  1. La petición llega al servidor Node.js

  2. Angular ejecuta ArticlesComponent en el servidor

  3. ngOnInit corre, llama a la API, obtiene los datos

  4. El servidor genera el HTML con los artículos ya incluidos

  5. El navegador recibe HTML completo y lo muestra de inmediato

  6. JavaScript descarga en paralelo

  7. Hidratación: Angular reutiliza el DOM existente y añade los event listeners

Ese último paso es la hidratación. Angular no borra y vuelve a renderizar — reconoce el DOM que ya existe y lo "activa".

withEventReplay() captura los clicks y eventos que el usuario pueda hacer durante esos milisegundos en que la hidratación aún no ha terminado, y los reproduce después. Sin esto, si alguien hace click en un botón antes de que Angular hidrate, ese click se pierde.

Cuándo tiene sentido: páginas de producto, artículos, feeds de noticias, resultados de búsqueda. Cualquier contenido público, dinámico y que necesite SEO.

Cuándo no: páginas muy personalizadas por usuario (cookies, sesión) que son difíciles de renderizar en servidor sin información del cliente. Para esas, CSR sigue siendo una opción válida.

Source: Server-side rendering • Angular


SSG — Static Site Generation (Prerendering)

SSG genera el HTML en tiempo de build, no en cada petición. Cuando el usuario visita la página, recibe un archivo HTML estático servido directamente desde un CDN o servidor de archivos. No hay Node.js procesando nada.

# Añadir SSR con prerendering
ng add @angular/ssr

# Construir con prerendering activado
ng build --prerender

O en angular.json:

{
  "projects": {
    "mi-blog": {
      "architect": {
        "build": {
          "options": {
            "prerender": true
          }
        }
      }
    }
  }
}

Lo que ocurre con el ejemplo:

  1. Al ejecutar ng build --prerender, Angular detecta todas las rutas

  2. Para cada ruta, ejecuta el componente como haría SSR

  3. Genera archivos .html estáticos con el contenido ya incluido

  4. Esos archivos se suben a un CDN

dist/
  browser/
    index.html          ← página principal prerenderizada
    articles/
      index.html        ← lista de artículos prerenderizada

El usuario recibe el HTML en milisegundos desde el CDN. No hay servidor calculando nada.

El problema con rutas dinámicas

Si tienes una ruta /articles/:id, Angular necesita saber qué IDs prerenderizar. Para eso existe getPrerenderParams().

Primero necesitas crear el archivo de rutas del servidor:

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'articles/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const res = await fetch('https://api.example.com/articles');
      const articles = await res.json();
      return articles.map((a: Article) => ({ id: String(a.id) }));
    }
  }
];

Angular ejecuta getPrerenderParams() durante el build, obtiene todos los IDs y genera un HTML estático por cada uno:

dist/
  browser/
    articles/
      1/index.html
      2/index.html
      3/index.html

Cuándo tiene sentido: páginas de marketing, documentación, blogs con contenido que no cambia cada hora, catálogos de producto estables. Si el contenido cambia una vez al día como mucho, SSG funciona bien.

Cuándo no: contenido que cambia frecuentemente (precios en tiempo real, stock, feeds de redes sociales) o que depende del usuario.

Source: Build-time prerendering • Angular


Renderizado híbrido: las tres estrategias en la misma app

La realidad es que casi ninguna aplicación real encaja perfectamente en una sola estrategia. Un blog tiene una página de inicio pública (SSG), artículos individuales (SSG), pero también un panel de administración (CSR) y una sección de comentarios en tiempo real (SSR).

Angular 19 introdujo la API de renderizado por ruta en app.routes.server.ts. Con ella puedes asignar una estrategia diferente a cada ruta:

# Crear proyecto con soporte de rutas de servidor
ng new mi-blog --ssr --server-routing
# o añadirlo a uno existente
ng add @angular/ssr --server-routing
// app.routes.server.ts
import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // Página principal: prerenderizada en build time (SSG)
  {
    path: '',
    renderMode: RenderMode.Prerender,
  },

  // Lista de artículos: prerenderizada
  {
    path: 'articles',
    renderMode: RenderMode.Prerender,
  },

  // Artículo individual: prerenderizado con IDs dinámicos
  {
    path: 'articles/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const res = await fetch('https://api.example.com/articles');
      const articles = await res.json();
      return articles.map((a: Article) => ({ id: String(a.id) }));
    },
  },

  // Sección de comentarios: SSR (contenido dinámico en tiempo real)
  {
    path: 'articles/:id/comments',
    renderMode: RenderMode.Server,
  },

  // Panel de administración: CSR (solo usuarios autenticados, sin SEO)
  {
    path: 'admin',
    renderMode: RenderMode.Client,
  },

  // Perfil de usuario: SSR (contenido específico por usuario)
  {
    path: 'profile',
    renderMode: RenderMode.Server,
  },

  // Cualquier ruta no definida: SSR como fallback
  {
    path: '**',
    renderMode: RenderMode.Server,
  },
];

Y registrar las rutas en la configuración del servidor:

// app.config.server.ts
import { ApplicationConfig } from '@angular/core';
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
import { serverRoutes } from './app.routes.server';

export const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    provideServerRouting(serverRoutes),
  ]
};

El componente ArticlesComponent no cambia. La misma implementación se prerenderiza, se renderiza en servidor o se ejecuta en cliente dependiendo de la ruta desde la que se accede.

Source: Hybrid rendering with server routing • Angular


Hidratación incremental

Una vez que tienes SSR o SSG activo, puedes ir un paso más allá con la hidratación incremental. En lugar de hidratar toda la página de golpe cuando JavaScript carga, cada sección se hidrata de forma independiente según cuándo se necesita.

// app.config.ts
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      withIncrementalHydration()
    ),
  ]
};

Con esto activado, puedes usar @defer con hydrate en lugar de on:

<!-- articles.component.html -->

<!-- El título hidrata de inmediato (parte crítica) -->
<h1>{{ title }}</h1>

<!-- La lista principal hidrata cuando entra en el viewport -->
@defer (hydrate on viewport) {
  <app-articles-list [articles]="articles()" />
} @placeholder {
  <app-articles-skeleton />
}

<!-- El widget de newsletter solo hidrata si el usuario hace hover -->
@defer (hydrate on hover) {
  <app-newsletter-widget />
} @placeholder {
  <div class="newsletter-placeholder">Suscríbete</div>
}

<!-- Los comentarios hidratan al hacer click en "Ver comentarios" -->
@defer (hydrate on interaction) {
  <app-comments [articleId]="articleId()" />
} @placeholder {
  <button>Ver comentarios</button>
}

El HTML completo llega prerenderizado o desde servidor. Solo el JavaScript necesario para cada sección se descarga y ejecuta cuando esa sección lo necesita.

Source: Incremental Hydration • Angular


Resumen visual

Petición del usuario
¿Cómo llega el HTML?
        ├── CSR: HTML vacío → JS descarga → Angular renderiza en browser
        │         Uso: dashboards, apps internas, herramientas autenticadas
        ├── SSR: Angular corre en servidor → HTML completo → browser hidrata
        │         Uso: páginas públicas con contenido dinámico o por usuario
        └── SSG: HTML generado en build → CDN sirve archivo estático
                  Uso: marketing, docs, blogs con contenido que no cambia a menudo
app.routes.server.ts (renderizado híbrido)
        ├── '/'               → RenderMode.Prerender  (SSG)
        ├── 'articles'        → RenderMode.Prerender  (SSG)
        ├── 'articles/:id'    → RenderMode.Prerender  (SSG + getPrerenderParams)
        ├── 'comments'        → RenderMode.Server     (SSR)
        ├── 'admin'           → RenderMode.Client     (CSR)
        └── '**'              → RenderMode.Server     (SSR fallback)

Tabla comparativa

CSR

SSR

SSG

Primera carga visible

Lenta

Rápida

Muy rápida

SEO

Malo

Bueno

Excelente

Servidor necesario

No

Sí (Node.js)

No (CDN)

Contenido dinámico

No

Contenido por usuario

No

Coste de infra

Bajo

Medio-alto

Muy bajo

Complejidad

Baja

Media

Baja-media


Una cosa a tener en cuenta

Con SSR hay código que no puedes ejecutar en el servidor: acceso a window, document, localStorage, o cualquier API exclusiva del browser.

Angular proporciona afterNextRender para esos casos:

// browser-only.component.ts
import { Component, afterNextRender } from '@angular/core';

@Component({ ... })
export class BrowserOnlyComponent {
  constructor() {
    afterNextRender(() => {
      // Este bloque solo se ejecuta en el browser, nunca en servidor
      const stored = localStorage.getItem('preferences');
    });
  }
}

Si tienes un componente de terceros que manipula el DOM directamente y rompe la hidratación, puedes marcarlo para que Angular no intente hidratarlo:

<app-legacy-chart ngSkipHydration />

Esto le dice a Angular que destruya y vuelva a renderizar ese componente en el cliente en lugar de intentar reutilizar el DOM del servidor.


Keywords

  • Angular SSR SSG CSR renderizado

  • Angular hybrid rendering

  • app.routes.server.ts Angular 19

  • Angular provideClientHydration

  • incremental hydration Angular