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 |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | Sei qui - SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida 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.
- Richiesta HTTP: Il browser (o un crawler) richiede una URL
- Rendering lato server: Express riceve la richiesta, Angular esegue l'app e produce l'HTML
- Invio HTML: Il server invia l'HTML completo con il contenuto renderizzato
- First Contentful Paint: Il browser visualizza immediatamente il contenuto
- Download JS: Il browser scarica i bundle JavaScript dell'applicazione
- Hydration: Angular "rianima" l'HTML statico, collegando event listener e reattività
- Applicazione interattiva: L'app diventa completamente funzionale
# 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
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,documentolocalStoragedurante il rendering server - Uso di
Date.now()oMath.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.
<!-- 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
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 ancheprovideClientHydration() - Il contenuto dentro
@deferviene comunque renderizzato sul server: il trigger controlla solo quando viene idratato sul client - I trigger
@placeholdere@loadingdi@defernon 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
// 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
// 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
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:
- Cattura: Un piccolo script inline (iniettato nell'HTML dal server) cattura tutti gli eventi utente prima che Angular sia pronto
- Buffer: Gli eventi vengono memorizzati in una coda con tutte le informazioni necessarie (tipo evento, target, timestamp)
- Replay: Quando la hydration del componente è completata, gli eventi vengono ri-dispatchati nell'ordine corretto
// 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:
- L'evento viene catturato da jsaction
- Il click trigger attiva l'hydration del componente
- Angular scarica il codice lazy del componente e lo idrata
- L'evento catturato viene ri-dispatchato sul componente ora interattivo
- 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
// 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.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)
// 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
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.
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.
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 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
// 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
windowfuori dai guard: Usa sempreisPlatformBrowseroafterNextRender - 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 neverper 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.

Commenti
Caricamento commenti...
Accedi per commentare