Salta al contenuto principale

6 - Angular SSR e Incremental Hydration

Server-Side Rendering in Angular 21: full hydration, incremental hydration con @defer, event replay, prerendering SSG e ottimizzazione SEO.

Cosa ne pensi di questo articolo?

6 - Angular SSR e Incremental Hydration
Angular ModernoFoto:Bibek ghoshPexels

In Breve

Server-Side Rendering in Angular 21: full hydration, incremental hydration con @defer, event replay, prerendering SSG e ottimizzazione SEO.

Angular SSR e Incremental Hydration: Rendering Lato Server nel 2025

Il Server-Side Rendering (SSR) ha attraversato una trasformazione radicale in Angular. Da Angular Universal come pacchetto esterno e sperimentale, il SSR è oggi una funzionalità di prima classe integrata nel core del framework. Con Angular 19+ l'introduzione della Incremental Hydration ha cambiato le regole del gioco: non serve più idratare l'intera applicazione in un colpo solo, ma solo le parti che l'utente effettivamente utilizza.

In questo sesto articolo della serie Angular Moderno esploreremo l'architettura SSR di Angular dalla configurazione base fino alle strategie avanzate di hydration incrementale, event replay, prerendering statico e ottimizzazione SEO. Vedremo come queste tecnologie impattano concretamente i Core Web Vitals è come configurare un'applicazione Angular per ottenere il massimo delle performance.

Cosa Imparerai in Questo Articolo

  • Come funziona il Server-Side Rendering in Angular e la sua evoluzione da Angular Universal
  • Il meccanismo di Full Application Hydration: riuso del DOM e replay degli eventi
  • I limiti della Full Hydration e il suo impatto sul Time to Interactive (TTI)
  • Incremental Hydration con i trigger @defer: viewport, interaction, idle, timer
  • Configurazione completa SSR: app.config.server.ts, Express server, provider
  • Event Replay e integrazione con jsaction per catturare interazioni durante l'hydration
  • Prerendering (SSG) per generazione statica delle pagine
  • Ottimizzazione SEO con meta tag, structured data e rendering per crawler
  • Pattern pratici: isPlatformBrowser, TransferState, API browser-only
  • Strategie di performance: partial hydration, ottimizzazione bundle, Core Web Vitals

Panoramica della Serie Angular Moderno

# Articolo Focus
1Angular SignalsReattività fine-grained
2Zoneless Change DetectionEliminare Zone.js
3Nuovi Template @if, @for, @deferControl flow moderno
4Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6Sei qui - SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Angular PWAProgressive Web App
9Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida alla migrazione

1. Server-Side Rendering in Angular

Il Server-Side Rendering consiste nel generare l'HTML di una pagina sul server anziche nel browser. Quando un utente o un crawler richiede una pagina, il server esegue l'applicazione Angular, produce l'HTML completo e lo invia al client. Il browser visualizza immediatamente il contenuto senza dover aspettare il download e l'esecuzione di JavaScript.

L'Evoluzione: da Angular Universal al Core

Storicamente il SSR in Angular era gestito da Angular Universal, un pacchetto separato (@nguniversal/express-engine) che richiedeva configurazione manuale significativa. Con Angular 17+ il SSR è stato integrato direttamente nel framework: @angular/ssr ha sostituito Angular Universal e la CLI genera automaticamente i file di configurazione necessari.

Evoluzione del SSR in Angular

Versione Tecnologia SSR Caratteristiche
Angular 2-16 Angular Universal (@nguniversal) Pacchetto esterno, configurazione manuale, nessuna hydration
Angular 16 Angular Universal + Hydration (Developer Preview) Prima hydration non-destructive, riuso del DOM
Angular 17 @angular/ssr (nuovo pacchetto) SSR integrato nella CLI, hydration stabile, @defer support
Angular 19 @angular/ssr + Incremental Hydration Hydration incrementale, event replay, route-level rendering
Angular 21 @angular/ssr (stabile) Incremental hydration stabile, SSG migliorato, performance ottimizzate

Come Funziona il Flusso SSR

Il ciclo di vita di una richiesta SSR in Angular segue un flusso preciso che coinvolge server, browser e il processo di hydration.

  1. Richiesta HTTP: Il browser (o un crawler) richiede una URL
  2. Rendering lato server: Express riceve la richiesta, Angular esegue l'app e produce l'HTML
  3. Invio HTML: Il server invia l'HTML completo con il contenuto renderizzato
  4. First Contentful Paint: Il browser visualizza immediatamente il contenuto
  5. Download JS: Il browser scarica i bundle JavaScript dell'applicazione
  6. Hydration: Angular "rianima" l'HTML statico, collegando event listener e reattività
  7. Applicazione interattiva: L'app diventa completamente funzionale
Creare un nuovo progetto Angular con SSR

# Nuovo progetto con SSR abilitato
ng new my-ssr-app --ssr

# Aggiungere SSR a un progetto esistente
ng add @angular/ssr

# Il comando genera automaticamente:
# - server.ts (Express server)
# - src/app/app.config.server.ts (configurazione server)
# - Aggiorna angular.json con target "server"
    

2. Full Application Hydration

La hydration è il processo attraverso il quale Angular trasforma l'HTML statico generato dal server in un'applicazione interattiva nel browser. Prima di Angular 16, il framework semplicemente distruggeva l'HTML del server e ri-renderizzava tutto da zero nel browser (destructive hydration). Questo causava un fastidioso "flash" del contenuto e vanificava gran parte dei benefici del SSR.

Non-Destructive Hydration

A partire da Angular 16 la hydration è diventata non-destructive: Angular riutilizza i nodi DOM generati dal server anziche ricrearli. Questo elimina il flash visivo e migliora significativamente il Largest Contentful Paint (LCP) perchè il browser non deve rifare il layout.

app.config.ts - Abilitare la hydration

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),

    // Abilita la hydration non-destructive
    provideClientHydration(),
  ]
};
    

Come Funziona il Riuso del DOM

Durante il rendering lato server, Angular serializza informazioni strutturali nell'HTML attraverso attributi speciali come ngSkipHydration e commenti nascosti. Quando il client riceve l'HTML e avvia la hydration, il framework:

  • Attraversa il DOM esistente anziche crearne uno nuovo
  • Collega gli event listener ai nodi DOM già presenti
  • Inizializza lo stato interno dei componenti
  • Verifica che il DOM del server corrisponda a quello che il client produrrebbe

Hydration Mismatch: Errori Comuni

Se l'HTML generato dal server non corrisponde a quello che il client si aspetta, Angular genera un errore di hydration mismatch. Le cause più comuni sono:

  • Accesso a window, document o localStorage durante il rendering server
  • Uso di Date.now() o Math.random() che producono valori diversi su server e client
  • Manipolazione diretta del DOM con ElementRef.nativeElement
  • Contenuto condizionale basato su feature browser-only (es. dimensione viewport)
  • Librerie di terze parti che modificano il DOM direttamente

3. Il Problema della Full Hydration

Con la Full Application Hydration, Angular deve idratare l'intera applicazione prima che diventi interattiva. Questo significa che anche componenti che l'utente non vedra mai (come il footer in fondo alla pagina o sezioni nascoste) vengono idratati al caricamento, con un impatto diretto sul Time to Interactive (TTI).

Impatto della Full Hydration sui Core Web Vitals

Metrica Senza SSR (CSR) SSR + Full Hydration Problema
First Contentful Paint (FCP) 2.5s 0.8s Migliorato significativamente
Largest Contentful Paint (LCP) 3.2s 1.2s Migliorato significativamente
Time to Interactive (TTI) 3.5s 3.0s Migliora poco: tutta l'app va idratata
Total Blocking Time (TBT) 800ms 650ms Ancora alto: hydration è un long task
Interaction to Next Paint (INP) 120ms 180ms Peggiora: eventi persi durante hydration

Il paradosso della Full Hydration è che migliora le metriche di rendering iniziale (FCP, LCP) ma non risolve, e a volte peggiora, le metriche di interattivita. Il motivo è semplice: idratare centinaia di componenti è un'operazione CPU-intensive che blocca il main thread.

perchè l'Hydration Completa e Costosa

  • Download di tutto il JavaScript: Anche per componenti mai visibili, il browser deve scaricare e parsare il codice
  • Esecuzione di ogni componente: Angular deve inizializzare ogni componente, creare il dependency injection tree e collegare i binding
  • Blocco del main thread: L'hydration avviene sincronamente, bloccando interazioni utente
  • Spreco di risorse su mobile: Dispositivi con CPU e banda limitati soffrono maggiormente

4. Incremental Hydration

La Incremental Hydration risolve il problema della Full Hydration permettendo di idratare i componenti on demand, solo quando servono. Introdotta come developer preview in Angular 19 è stabilizzata in Angular 21, questa funzionalità si basa sul blocco @defer già presente nel template syntax di Angular.

Come Funziona

L'idea è semplice ma potente: i componenti avvolti in un blocco @defer vengono renderizzati normalmente sul server (l'utente vede il contenuto), ma nel client la loro hydration viene posticipata fino a quando un trigger specifico non viene attivato. Nel frattempo, il DOM statico del server resta visibile ma non interattivo.

Incremental Hydration con @defer e trigger hydrate

<!-- Questa sezione viene renderizzata sul server ma idratata
     solo quando entra nel viewport dell'utente -->
@defer (hydrate on viewport) {
  <app-product-list [category]="selectedCategory" />
}

<!-- Questo componente viene idratato quando l'utente
     interagisce con esso (click, focus, hover) -->
@defer (hydrate on interaction) {
  <app-comment-section [postId]="post.id" />
}

<!-- Questo viene idratato quando il browser e idle,
     cioe non sta eseguendo altri task -->
@defer (hydrate on idle) {
  <app-sidebar-recommendations />
}

<!-- Questo viene idratato dopo un timer specifico -->
@defer (hydrate on timer(3s)) {
  <app-analytics-widget />
}

<!-- Mai idratato: resta HTML statico per sempre -->
@defer (hydrate never) {
  <app-static-footer />
}

<!-- Idratato immediatamente (equivale a full hydration) -->
@defer (hydrate on immediate) {
  <app-critical-cta />
}
    

Trigger di Hydration Disponibili

Trigger Sintassi Quando si Attiva Caso d'Uso
Viewport hydrate on viewport Il componente entra nel viewport visibile Contenuto below-the-fold, liste prodotti
Interaction hydrate on interaction Click, focus, touch sull'elemento Form, bottoni, aree interattive
Idle hydrate on idle Il browser non ha task in coda Widget secondari, sidebar
Timer hydrate on timer(Ns) Dopo N secondi dal caricamento Analytics, tracking, widget differiti
Immediate hydrate on immediate Subito al caricamento Componenti critici above-the-fold
Never hydrate never Mai: resta HTML statico Footer, contenuto puramente informativo

Abilitare la Incremental Hydration

app.config.ts - Configurazione per Incremental Hydration

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    // Abilita hydration con supporto incrementale
    provideClientHydration(
      withIncrementalHydration()
    ),
  ]
};
    

Requisiti per la Incremental Hydration

  • I componenti deferiti devono essere standalone (non possono dipendere da NgModule)
  • La funzione withIncrementalHydration() richiede anche provideClientHydration()
  • Il contenuto dentro @defer viene comunque renderizzato sul server: il trigger controlla solo quando viene idratato sul client
  • I trigger @placeholder e @loading di @defer non vengono usati in SSR: il server mostra direttamente il contenuto completo

5. Configurazione SSR Completa

Una configurazione SSR production-ready in Angular coinvolge diversi file. Vediamo la struttura completa e il ruolo di ogni componente.

Configurazione Server-Side

app.config.server.ts - Configurazione lato server

// src/app/app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRoutesConfig } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {
  providers: [
    // Abilita il rendering lato server
    provideServerRendering(),

    // Configurazione route-level rendering
    provideServerRoutesConfig(serverRoutes),
  ]
};

// Merge della config client con quella server
export const config = mergeApplicationConfig(appConfig, serverConfig);
    

Route-Level Rendering Configuration

app.routes.server.ts - Configurazione rendering per route

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

export const serverRoutes: ServerRoute[] = [
  {
    path: '',
    renderMode: RenderMode.Prerender,  // SSG: generato al build time
  },
  {
    path: 'blog',
    renderMode: RenderMode.Prerender,  // Pagina statica
  },
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Server,  // SSR: generato ad ogni richiesta
  },
  {
    path: 'dashboard',
    renderMode: RenderMode.Client,  // CSR: solo lato client
  },
  {
    path: '**',
    renderMode: RenderMode.Server,  // Default: SSR per tutto il resto
  },
];
    

Express Server

server.ts - Server Express per SSR

// server.ts
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './src/main.server';

export function app(): express.Express {
  const server = express();
  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  const browserDistFolder = resolve(serverDistFolder, '../browser');
  const indexHtml = join(serverDistFolder, 'index.server.html');

  const commonEngine = new CommonEngine();

  // Serve file statici dalla cartella browser
  server.get('*.*', express.static(browserDistFolder, {
    maxAge: '1y',   // Cache aggressiva per asset statici
    index: false,
  }));

  // Tutte le altre route passano per Angular SSR
  server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [
          { provide: APP_BASE_HREF, useValue: baseUrl }
        ],
      })
      .then(html => res.send(html))
      .catch(err => next(err));
  });

  return server;
}

function run(): void {
  const port = process.env['PORT'] || 4000;
  const server = app();

  server.listen(port, () => {
    console.log(`Server SSR in ascolto su http://localhost:${port}`);
  });
}

run();
    

Struttura File SSR

File Ruolo
src/app/app.config.ts Configurazione client: router, hydration, provider
src/app/app.config.server.ts Configurazione server: provideServerRendering, serverRoutes
src/app/app.routes.server.ts Definisce renderMode per ogni route (Prerender, Server, Client)
server.ts Express server che gestisce le richieste SSR
src/main.server.ts Entry point per il bootstrap lato server

6. Event Replay

Uno dei problemi più frustranti del SSR tradizionale è il cosiddetto "uncanny valley": l'utente vede la pagina renderizzata e inizia a interagire (clicca bottoni, compila form), ma l'applicazione non risponde perchè la hydration non è ancora completata. Gli eventi vengono persi e l'utente deve ripetere le azioni.

Come Funziona l'Event Replay

Angular risolve questo problema con l'Event Replay, basato sulla libreria jsaction di Google. Il meccanismo funziona in tre fasi:

  1. Cattura: Un piccolo script inline (iniettato nell'HTML dal server) cattura tutti gli eventi utente prima che Angular sia pronto
  2. Buffer: Gli eventi vengono memorizzati in una coda con tutte le informazioni necessarie (tipo evento, target, timestamp)
  3. Replay: Quando la hydration del componente è completata, gli eventi vengono ri-dispatchati nell'ordine corretto
Abilitare Event Replay nella configurazione

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      // Event Replay: cattura gli eventi durante la hydration
      withEventReplay(),
      // Incremental Hydration: hydrate on demand
      withIncrementalHydration(),
    ),
  ]
};
    

Event Replay: Comportamento con Incremental Hydration

Con la Incremental Hydration, l'Event Replay diventa ancora più importante. Quando un utente clicca su un componente la cui hydration è posticipata (es. hydrate on interaction), accade quanto segue:

  1. L'evento viene catturato da jsaction
  2. Il click trigger attiva l'hydration del componente
  3. Angular scarica il codice lazy del componente e lo idrata
  4. L'evento catturato viene ri-dispatchato sul componente ora interattivo
  5. L'utente non percepisce alcun ritardo significativo

7. Prerendering (SSG)

Il Prerendering (o Static Site Generation, SSG) genera l'HTML delle pagine al build time anziche ad ogni richiesta. Le pagine vengono salvate come file HTML statici e servite direttamente da un CDN o da un web server senza necessità di un server Node.js in esecuzione.

Quando Usare SSG vs SSR

SSG vs SSR: Guida alla Scelta

Criterio SSG (Prerender) SSR (Server)
Contenuto Statico, cambia raramente Dinamico, personalizzato per utente
Performance Velocissimo (file serviti da CDN) Dipende dal tempo di rendering server
Infrastruttura Solo hosting statico (Firebase, Netlify, S3) Server Node.js in esecuzione
Scalabilità Eccellente (CDN gestisce il carico) Richiede scaling del server
Aggiornamento Richiede rebuild e deploy Contenuto sempre aggiornato
Casi d'uso tipici Blog, landing page, documentazione, portfolio E-commerce, dashboard, social media

Configurare il Prerendering

Configurazione route per prerendering con route parametriche

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

export const serverRoutes: ServerRoute[] = [
  // Pagine statiche: generate al build time
  {
    path: '',
    renderMode: RenderMode.Prerender,
  },
  {
    path: 'about',
    renderMode: RenderMode.Prerender,
  },

  // Route parametriche: elenco esplicito di path
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      // Restituisce un array di oggetti con i parametri
      return [
        { slug: 'angular-ssr-hydration' },
        { slug: 'angular-signals-guida' },
        { slug: 'zoneless-change-detection' },
      ];
    },
  },

  // Route dinamiche: SSR a runtime
  {
    path: 'product/:id',
    renderMode: RenderMode.Server,
  },

  // Dashboard: solo client-side
  {
    path: 'admin/**',
    renderMode: RenderMode.Client,
  },
];
    

RenderMode: Le Tre Strategie

  • RenderMode.Prerender (SSG): HTML generato al build time, ideale per contenuti statici. Velocissimo, nessun server necessario a runtime
  • RenderMode.Server (SSR): HTML generato ad ogni richiesta sul server. Necessario per contenuti personalizzati o che cambiano frequentemente
  • RenderMode.Client (CSR): Nessun rendering server, l'HTML viene generato interamente dal browser. Utile per aree protette o dashboard interattive

8. SEO con SSR

Il SSR è fondamentale per la SEO perchè i crawler dei motori di ricerca possono leggere direttamente l'HTML generato dal server senza dover eseguire JavaScript. Ma il rendering da solo non basta: servono meta tag strutturati, dati JSON-LD è una corretta gestione delle URL canoniche.

Meta Tags Dinamici con il Title e Meta Service

SEO Service per gestione meta tag in SSR

// seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
import { DOCUMENT } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class SeoService {
  private title = inject(Title);
  private meta = inject(Meta);
  private document = inject(DOCUMENT);

  updatePageMeta(config: {
    title: string;
    description: string;
    url: string;
    image?: string;
    type?: string;
  }): void {
    // Title tag
    this.title.setTitle(config.title);

    // Meta description
    this.meta.updateTag({
      name: 'description',
      content: config.description
    });

    // Open Graph tags
    this.meta.updateTag({ property: 'og:title', content: config.title });
    this.meta.updateTag({ property: 'og:description', content: config.description });
    this.meta.updateTag({ property: 'og:url', content: config.url });
    this.meta.updateTag({ property: 'og:type', content: config.type || 'website' });

    if (config.image) {
      this.meta.updateTag({ property: 'og:image', content: config.image });
    }

    // Twitter Card
    this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
    this.meta.updateTag({ name: 'twitter:title', content: config.title });
    this.meta.updateTag({ name: 'twitter:description', content: config.description });

    // Canonical URL
    this.setCanonicalUrl(config.url);
  }

  addStructuredData(data: object): void {
    const script = this.document.createElement('script');
    script.type = 'application/ld+json';
    script.textContent = JSON.stringify(data);
    this.document.head.appendChild(script);
  }

  private setCanonicalUrl(url: string): void {
    let link: HTMLLinkElement | null =
      this.document.querySelector('link[rel="canonical"]');
    if (!link) {
      link = this.document.createElement('link');
      link.setAttribute('rel', 'canonical');
      this.document.head.appendChild(link);
    }
    link.setAttribute('href', url);
  }
}
    

Structured Data (JSON-LD)

Aggiungere structured data per articoli blog

// In un componente articolo blog
@Component({ /* ... */ })
export class ArticlePageComponent {
  private seo = inject(SeoService);

  ngOnInit(): void {
    const article = this.articleData();

    this.seo.updatePageMeta({
      title: `${article.title} | Federico Calo`,
      description: article.summary,
      url: `https://federicocalo.dev/blog/${article.slug}`,
      image: 'https://federicocalo.dev/assets/og-image.jpg',
      type: 'article',
    });

    // Structured Data per Google
    this.seo.addStructuredData({
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: article.title,
      description: article.summary,
      author: {
        '@type': 'Person',
        name: 'Federico Calo',
        url: 'https://federicocalo.dev',
      },
      datePublished: article.date,
      image: 'https://federicocalo.dev/assets/og-image.jpg',
      publisher: {
        '@type': 'Organization',
        name: 'Federico Calo Dev',
        url: 'https://federicocalo.dev',
      },
    });
  }
}
    

SEO Checklist per Applicazioni SSR Angular

  • Title tag unico per pagina: Massimo 60 caratteri, keyword principale inclusa
  • Meta description: 150-160 caratteri, riassunto accattivante della pagina
  • Open Graph e Twitter Card: Per una corretta anteprima sui social media
  • Canonical URL: Previene problemi di contenuto duplicato
  • JSON-LD structured data: Article, Person, Organization, BreadcrumbList
  • Sitemap.xml: Generato automaticamente con hreflang per siti multilingua
  • robots.txt: Configurato per consentire il crawling delle pagine importanti
  • Heading hierarchy: Un solo H1 per pagina, H2-H6 in ordine logico

9. Pattern Pratici per SSR

Lavorare con SSR richiede attenzione alle differenze tra ambiente server e browser. Molte API del browser (come window, localStorage, document.querySelector) non sono disponibili durante il rendering lato server. Ecco i pattern essenziali per gestire queste situazioni.

isPlatformBrowser e isPlatformServer

Controllo della piattaforma con PLATFORM_ID

import { Component, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({
  selector: 'app-analytics',
  standalone: true,
  template: `<div #trackingContainer></div>`
})
export class AnalyticsComponent {
  private platformId = inject(PLATFORM_ID);

  ngOnInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      // Sicuro: eseguito solo nel browser
      this.initGoogleAnalytics();
      this.trackPageView();
    }

    if (isPlatformServer(this.platformId)) {
      // Eseguito solo sul server
      console.log('Rendering lato server per:', this.route);
    }
  }

  private initGoogleAnalytics(): void {
    // Accesso sicuro a window e document
    const gtag = (window as any).gtag;
    if (gtag) {
      gtag('config', 'G-XXXXXXXXXX');
    }
  }

  private trackPageView(): void {
    // localStorage disponibile solo nel browser
    const views = localStorage.getItem('pageViews') || '0';
    localStorage.setItem('pageViews', String(Number(views) + 1));
  }
}
    

TransferState: Evitare Richieste HTTP Duplicate

Quando il server effettua una richiesta API per renderizzare una pagina, il client non dovrebbe ripetere la stessa richiesta. Il TransferState permette di trasferire i dati dal server al client incorporandoli nell'HTML.

TransferState per evitare richieste duplicate

import {
  Injectable, inject, PLATFORM_ID,
  TransferState, makeStateKey
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

interface Product {
  id: number;
  name: string;
  price: number;
}

const PRODUCTS_KEY = makeStateKey<Product[]>('products');

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private transferState = inject(TransferState);
  private platformId = inject(PLATFORM_ID);

  getProducts(): Observable<Product[]> {
    // 1. Controlla se i dati sono già nel TransferState
    const cached = this.transferState.get(PRODUCTS_KEY, null);
    if (cached) {
      // Dati trasferiti dal server: usali e rimuovili
      this.transferState.remove(PRODUCTS_KEY);
      return of(cached);
    }

    // 2. Nessun dato in cache: fai la richiesta HTTP
    return this.http.get<Product[]>('/api/products').pipe(
      tap(products => {
        if (isPlatformServer(this.platformId)) {
          // 3. Sul server: salva nel TransferState
          this.transferState.set(PRODUCTS_KEY, products);
        }
      })
    );
  }
}
    

HttpClient e TransferState Automatico

A partire da Angular 18, provideClientHydration() include automaticamente il caching delle richieste HTTP tramite withHttpTransferCacheOptions(). Questo significa che nella maggior parte dei casi non devi gestire manualmente il TransferState per le chiamate HTTP: Angular lo fa per te.

afterNextRender e afterRender

Per codice che deve essere eseguito solo nel browser dopo il rendering, Angular fornisce due API dedicate che sono SSR-safe e zoneless-compatible.

afterNextRender per inizializzazione browser-only

import { Component, afterNextRender, afterRender, ElementRef, viewChild } from '@angular/core';

@Component({
  selector: 'app-chart',
  standalone: true,
  template: `<canvas #chartCanvas width="800" height="400"></canvas>`
})
export class ChartComponent {
  canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');

  constructor() {
    // Eseguito UNA VOLTA dopo il primo render nel browser
    // NON eseguito sul server
    afterNextRender(() => {
      const ctx = this.canvas().nativeElement.getContext('2d');
      this.initChart(ctx!);
    });

    // Eseguito OGNI VOLTA che Angular rende il componente
    // Utile per sincronizzare con librerie DOM esterne
    afterRender(() => {
      this.updateChartDimensions();
    });
  }

  private initChart(ctx: CanvasRenderingContext2D): void {
    // Inizializza un chart con Canvas API
    // Sicuro: eseguito solo nel browser
  }

  private updateChartDimensions(): void {
    // Aggiorna dimensioni basate sul viewport
  }
}
    

10. Ottimizzazione Performance

SSR e Incremental Hydration hanno un impatto diretto sui Core Web Vitals. Vediamo strategie concrete per massimizzare le performance.

Strategia di Hydration per Tipo di Contenuto

Mappa delle Strategie di Hydration

Zona della Pagina Strategia Motivazione
Navbar e Hero section hydrate on immediate Above-the-fold, interazione immediata necessaria
Contenuto principale (articolo, prodotto) Full hydration (no @defer) Core content, LCP element probabile
Sezione commenti hydrate on viewport Below-the-fold, visibile solo con scroll
Form di contatto hydrate on interaction Interattivo ma non critico al primo caricamento
Sidebar raccomandazioni hydrate on idle Secondario, da idratare quando il browser e libero
Widget analytics/tracking hydrate on timer(5s) Non visibile, può aspettare
Footer e link legali hydrate never Puramente informativo, non necessità interattivita

Impatto sui Core Web Vitals

Benchmark: Applicazione Blog con 50+ Articoli

Metrica CSR Only SSR + Full Hydration SSR + Incremental Hydration
FCP 2.4s 0.8s 0.7s
LCP 3.1s 1.1s 0.9s
TTI 3.8s 3.2s 1.5s
TBT 850ms 620ms 180ms
INP 110ms 170ms 65ms
CLS 0.15 0.02 0.01
JS iniziale 320KB 320KB 85KB

Ottimizzazione del Bundle con Incremental Hydration

Layout ottimizzato con strategie di hydration differenziate

<!-- Layout pagina prodotto ottimizzato per Core Web Vitals -->

<!-- CRITICO: hydration immediata (above-the-fold) -->
<app-navbar />
<app-product-hero [product]="product()" />

<!-- VISIBILITA: hydration su viewport -->
@defer (hydrate on viewport) {
  <app-product-details [product]="product()" />
}

@defer (hydrate on viewport) {
  <app-product-reviews [productId]="product().id" />
}

<!-- INTERAZIONE: hydration su input utente -->
@defer (hydrate on interaction) {
  <app-add-to-cart [product]="product()" />
}

<!-- BACKGROUND: hydration quando il browser e libero -->
@defer (hydrate on idle) {
  <app-related-products [categoryId]="product().categoryId" />
}

<!-- DIFFERITO: hydration dopo timer -->
@defer (hydrate on timer(5s)) {
  <app-user-tracking [productId]="product().id" />
}

<!-- STATICO: mai idratato -->
@defer (hydrate never) {
  <app-footer />
}
    

Misurare l'Impatto

Monitorare Core Web Vitals in produzione

// performance-monitor.service.ts
import { Injectable, inject, PLATFORM_ID, afterNextRender } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class PerformanceMonitorService {
  private platformId = inject(PLATFORM_ID);

  constructor() {
    afterNextRender(() => {
      this.observeWebVitals();
    });
  }

  private observeWebVitals(): void {
    if (!isPlatformBrowser(this.platformId)) return;

    // Largest Contentful Paint
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      console.log('LCP:', lastEntry.startTime, 'ms');
    });
    lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

    // First Input Delay
    const fidObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries() as PerformanceEventTiming[];
      entries.forEach(entry => {
        console.log('FID:', entry.processingStart - entry.startTime, 'ms');
      });
    });
    fidObserver.observe({ type: 'first-input', buffered: true });

    // Cumulative Layout Shift
    const clsObserver = new PerformanceObserver((list) => {
      let clsScore = 0;
      list.getEntries().forEach((entry: any) => {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      });
      console.log('CLS:', clsScore);
    });
    clsObserver.observe({ type: 'layout-shift', buffered: true });
  }
}
    

Errori Comuni da Evitare con SSR

  • Accesso a window fuori dai guard: Usa sempre isPlatformBrowser o afterNextRender
  • Hydration mismatch: Non generare contenuto diverso tra server e client (es. Date.now(), dati casuali)
  • Import di librerie browser-only: Librerie come Chart.js o Leaflet vanno importate con dynamic import nel browser
  • TransferState non utilizzato: Senza TransferState (o l'auto-cache di Angular 18+), le chiamate HTTP vengono fatte due volte
  • Hydration di tutto: Con Incremental Hydration disponibile, idratare tutta l'app è uno spreco. Usa hydrate never per contenuti statici
  • Ignorare CLS: Font web, immagini senza dimensioni e contenuti caricati dinamicamente causano layout shift

Riepilogo e Prossimi Passi

Il SSR in Angular ha fatto un salto qualitativo enorme: da Angular Universal come pacchetto esterno a una soluzione integrata, performante e flessibile. La Incremental Hydration rappresenta la vera rivoluzione, permettendo di combinare il miglior rendering iniziale possibile con un'interattivita immediata e mirata.

Concetti Chiave di Questo Articolo

  • SSR in Angular: Integrato nel core con @angular/ssr, sostituisce Angular Universal
  • Full Hydration: Riutilizza il DOM del server ma idrata tutto in una volta, impattando il TTI
  • Incremental Hydration: Idrata i componenti on-demand con trigger @defer (viewport, interaction, idle, timer, never)
  • Event Replay: Cattura le interazioni utente durante la hydration e le ri-dispatcha dopo
  • Prerendering (SSG): Genera HTML al build time per pagine statiche, servito da CDN
  • RenderMode: Scelta per-route tra Prerender, Server e Client
  • SEO: Meta tags, Open Graph, JSON-LD structured data per massimizzare la visibilità
  • Pattern SSR: isPlatformBrowser, TransferState, afterNextRender
  • Performance: Incremental Hydration riduce TTI del 50%+ e TBT del 70%+

Nel prossimo articolo della serie esploreremo i Core Web Vitals in Angular, analizzando in profondità come misurare, monitorare e ottimizzare FCP, LCP, CLS, INP e TTI per ottenere un punteggio Lighthouse di 90+ in tutte le categorie.

Domande frequenti

Qual è il Server-Side Rendering (SSR) in Angular?

Il Server-Side Rendering consiste nel generare l'HTML di una pagina sul server anziché nel browser.

Come funziona la Full Application Hydration in Angular?

La Full Application Hydration idrata l'intera applicazione prima che diventi interattiva, il che significa che anche componenti non visibili vengono idratati al caricamento.

Qual è lo scopo dell'Incremental Hydration in Angular?

L'Incremental Hydration permette di idratare i componenti on demand, solo quando servono, risolvendo il problema della Full Hydration.

Come funziona l'Event Replay in Angular?

L'Event Replay riproduce gli eventi persi durante la hydration, garantendo che l'applicazione risponda correttamente alle azioni dell'utente.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti