Ottimizzare Core Web Vitals in Angular
I Core Web Vitals sono le metriche di performance che Google utilizza come fattore di ranking per determinare la qualità dell'esperienza utente di un sito web. Per le applicazioni Angular, ottimizzare queste metriche non è solo una questione tecnica: è un investimento diretto sulla visibilità nei motori di ricerca e sulla soddisfazione degli utenti.
In questo settimo articolo della serie Angular Moderno esploreremo in profondità
cosa sono i Core Web Vitals, come misurarli, e soprattutto come ottimizzarli in un'applicazione
Angular con tecniche specifiche per LCP, INP e CLS. Dalla gestione delle immagini con
NgOptimizedImage alla riduzione del bundle con @defer, fino al
monitoraggio in produzione con Real User Monitoring.
Cosa Imparerai in Questo Articolo
- Cosa sono i Core Web Vitals e perchè sono un fattore di ranking Google
- Come ottimizzare il Largest Contentful Paint (LCP) con NgOptimizedImage e preconnect
- Strategie per migliorare Interaction to Next Paint (INP) con Signals e riduzione della change detection
- Come prevenire il Cumulative Layout Shift (CLS) con dimensioni esplicite e skeleton screen
- Ottimizzazioni Angular-specifiche: SSR, lazy loading con
@defer, route-based code splitting - Analisi e riduzione del bundle con tree shaking, minificazione e source-map-explorer
- Misurazione delle performance con web-vitals, Lighthouse e Chrome DevTools
- Ottimizzazione avanzata di immagini e font per il web
- Monitoraggio in produzione con Performance Observer API e GA4
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 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Sei qui → 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. Cosa Sono i Core Web Vitals
I Core Web Vitals sono un insieme di metriche definite da Google che misurano tre aspetti fondamentali dell'esperienza utente: velocità di caricamento, reattività alle interazioni e stabilità visiva. Dal 2021 fanno parte ufficialmente dei segnali di ranking di Google, il che significa che un sito con Core Web Vitals scadenti viene penalizzato nei risultati di ricerca.
Le Tre Metriche Core Web Vitals
| Metrica | Cosa Misura | Buono | Migliorabile | Scarso |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Tempo per renderizzare l'elemento più grande visibile | ≤ 2.5s | 2.5s - 4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Latenza tra interazione utente e aggiornamento visivo | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Spostamenti imprevisti degli elementi nella pagina | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
Da FID a INP: L'Evoluzione della Metrica di Reattività
A marzo 2024 Google ha sostituito il First Input Delay (FID) con Interaction to Next Paint (INP) come metrica ufficiale. La differenza fondamentale è che FID misurava solo il ritardo della prima interazione, mentre INP misura la latenza di tutte le interazioni durante l'intera sessione, restituendo il valore peggiore (al 98° percentile). Questo rende INP una metrica molto più rappresentativa dell'esperienza reale dell'utente.
Perchè Sono Importanti per Angular
Le applicazioni Angular, specialmente quelle Single Page Application (SPA) con rendering lato client, hanno storicamente problemi con i Core Web Vitals. I motivi principali sono:
- Bundle JavaScript pesanti: Angular include il framework, i componenti e le dipendenze in un unico bundle iniziale
- Change detection costosa: Zone.js intercetta ogni evento asincrono e trigghera cicli di change detection
- Rendering lato client: Senza SSR, il browser deve scaricare, parsare e eseguire tutto il JS prima di mostrare contenuto
- Lazy loading non ottimale: Senza strategie adeguate, tutto il codice viene caricato al primo accesso
2. Ottimizzare il Largest Contentful Paint (LCP)
Il LCP misura il tempo necessario per renderizzare l'elemento più grande visibile nel viewport iniziale. Tipicamente è un'immagine hero, un blocco di testo o un elemento video. Per Angular, il LCP è fortemente influenzato dal Server-Side Rendering, dal caricamento delle immagini e dalle risorse bloccanti.
NgOptimizedImage: La Direttiva Fondamentale
Angular fornisce la direttiva NgOptimizedImage nel pacchetto
@angular/common che applica automaticamente le best practice per il
caricamento delle immagini. Questa direttiva gestisce lazy loading, priorità, dimensioni
e formati moderni.
// hero.component.ts
import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
@Component({
selector: 'app-hero',
standalone: true,
imports: [NgOptimizedImage],
template: `
<section class="hero">
<!-- Immagine LCP: priority carica immediatamente -->
<img
ngSrc="assets/images/hero-banner.webp"
width="1200"
height="630"
priority
alt="Hero banner del portfolio"
/>
<!-- Immagini below-the-fold: lazy loading automatico -->
<img
ngSrc="assets/images/project-thumb.webp"
width="400"
height="300"
alt="Anteprima progetto"
/>
</section>
`
})
export class HeroComponent {}
Errore Comune: Dimenticare l'Attributo priority
Senza priority, NgOptimizedImage applica automaticamente
loading="lazy" a tutte le immagini. Per l'immagine LCP (tipicamente la hero
image above-the-fold) questo e controproducente perchè ritarda il
caricamento dell'elemento più importante. Aggiungi sempre priority alla
tua immagine LCP.
Preconnect e Preload per Risorse Critiche
Il browser deve risolvere DNS, stabilire connessioni TCP e negoziare TLS prima di scaricare
risorse da domini esterni. Con preconnect e preload possiamo
anticipare queste operazioni.
<head>
<!-- Preconnect ai domini delle risorse critiche -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.example.com" />
<!-- Preload dell'immagine LCP -->
<link
rel="preload"
as="image"
href="/assets/images/hero-banner.webp"
type="image/webp"
fetchpriority="high"
/>
<!-- Preload del font critico -->
<link
rel="preload"
as="font"
href="/assets/fonts/inter-v12-latin-regular.woff2"
type="font/woff2"
crossorigin
/>
</head>
Checklist LCP per Angular
- Usa
NgOptimizedImageconprioritysull'immagine LCP - Abilita SSR per inviare HTML con contenuto pre-renderizzato
- Aggiungi
preconnectper CDN immagini e font - Usa
preloadper l'immagine hero e il font critico - Converti le immagini in WebP o AVIF per ridurre la dimensione
- Implementa un image loader CDN per ridimensionamento automatico
- Evita redirect sulle risorse critiche (ogni redirect aggiunge 300-500ms)
- Minimizza il CSS critico e inline-alo nell'HTML
3. Ottimizzare Interaction to Next Paint (INP)
L'INP misura il tempo che intercorre tra un'interazione dell'utente (click, tap, pressione tasto) e il prossimo aggiornamento visivo sullo schermo. Per Angular questa metrica è direttamente influenzata dalla change detection, dagli event handler e dall'uso di Zone.js.
Signals per Ridurre la Change Detection
Una delle ottimizzazioni più impattanti per l'INP in Angular e il passaggio da Zone.js ai Signals. Con i Signals, Angular aggiorna solo i componenti i cui dati sono effettivamente cambiati, eliminando i cicli globali di change detection.
// product-list.component.ts - PRIMA (Zone.js, change detection globale)
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
<span>{{ product.name }}</span>
<button (click)="addToCart(product)">Aggiungi</button>
</div>
`
})
export class ProductListComponent {
products: Product[] = [];
addToCart(product: Product): void {
// Ogni click trigghera change detection su TUTTA l'app
this.cartService.add(product);
}
}
// product-list.component.ts - DOPO (Signals, aggiornamento mirato)
@Component({
selector: 'app-product-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (product of products(); track product.id) {
<div>
<span>{{ product.name }}</span>
<button (click)="addToCart(product)">Aggiungi</button>
</div>
}
`
})
export class ProductListComponent {
products = input.required<Product[]>();
private cartService = inject(CartService);
addToCart(product: Product): void {
// Solo questo componente viene aggiornato
this.cartService.add(product);
}
}
Evitare Long Task negli Event Handler
Un event handler che esegue calcoli pesanti blocca il main thread e peggiora l'INP.
La strategia è suddividere il lavoro in micro-task usando requestAnimationFrame
o setTimeout.
// Approccio ottimizzato: suddividi il lavoro pesante
export class DataProcessorComponent {
async onFilterChange(filter: string): Promise<void> {
// 1. Aggiorna immediatamente l'UI (feedback visivo)
this.isLoading.set(true);
// 2. Cedi il controllo al browser per il paint
await this.yieldToMain();
// 3. Esegui il lavoro pesante in chunk
const results = await this.processInChunks(this.rawData(), filter);
// 4. Aggiorna i risultati
this.filteredData.set(results);
this.isLoading.set(false);
}
private yieldToMain(): Promise<void> {
return new Promise(resolve => {
// scheduler.yield() dove disponibile, altrimenti setTimeout
if ('scheduler' in window && 'yield' in (window as any).scheduler) {
(window as any).scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
private async processInChunks(
data: Item[],
filter: string,
chunkSize = 500
): Promise<Item[]> {
const results: Item[] = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
results.push(...chunk.filter(item => item.name.includes(filter)));
// Cedi il main thread tra un chunk e l'altro
if (i + chunkSize < data.length) {
await this.yieldToMain();
}
}
return results;
}
}
Impatto di Zone.js sull'INP
| Scenario | Con Zone.js | Zoneless (Signals) | Miglioramento |
|---|---|---|---|
| Click su bottone in lista 100 elementi | 85ms | 12ms | -86% |
| Digitazione in campo ricerca | 120ms | 18ms | -85% |
| Toggle filtro in tabella 1000 righe | 250ms | 45ms | -82% |
| Navigazione tra tab | 65ms | 8ms | -88% |
4. Prevenire il Cumulative Layout Shift (CLS)
Il CLS misura la somma degli spostamenti imprevisti degli elementi nella pagina durante il suo ciclo di vita. Un CLS elevato crea un'esperienza frustrante: l'utente sta per cliccare un bottone e il layout si sposta, facendogli cliccare qualcos'altro.
Dimensioni Esplicite per Immagini e Media
La causa più comune di CLS sono immagini e iframe senza dimensioni esplicite. Quando il browser non conosce le dimensioni di un'immagine, alloca zero spazio fino al caricamento, poi il layout si ricalcola bruscamente.
<!-- MALE: nessuna dimensione, causa CLS -->
<img src="photo.jpg" alt="Foto" />
<!-- BENE: dimensioni esplicite, zero CLS -->
<img src="photo.jpg" alt="Foto" width="800" height="600" />
<!-- MEGLIO: NgOptimizedImage con fill per immagini responsive -->
<div class="image-container" style="position: relative; aspect-ratio: 16/9;">
<img ngSrc="photo.webp" fill alt="Foto responsive" />
</div>
<!-- Video e iframe: stessa regola -->
<iframe
src="https://www.youtube.com/embed/xxx"
width="560"
height="315"
loading="lazy"
title="Video tutorial"
></iframe>
Skeleton Screen per Contenuto Dinamico
Quando il contenuto viene caricato in modo asincrono (da API, database, ecc.), uno skeleton screen preserva lo spazio necessario e previene il layout shift.
// article-skeleton.component.ts
@Component({
selector: 'app-article-skeleton',
standalone: true,
template: `
<div class="skeleton-wrapper">
<div class="skeleton-image" style="aspect-ratio: 16/9;"></div>
<div class="skeleton-title" style="height: 32px; width: 80%;"></div>
<div class="skeleton-text" style="height: 16px; width: 100%;"></div>
<div class="skeleton-text" style="height: 16px; width: 95%;"></div>
<div class="skeleton-text" style="height: 16px; width: 60%;"></div>
</div>
`,
styles: [`
.skeleton-wrapper {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-image, .skeleton-title, .skeleton-text {
background: linear-gradient(90deg, #21262d 25%, #30363d 50%, #21262d 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`]
})
export class ArticleSkeletonComponent {}
// Utilizzo nel componente padre
@Component({
template: `
@if (article(); as art) {
<app-article [data]="art" />
} @else {
<app-article-skeleton />
}
`
})
export class ArticlePageComponent {
article = signal<Article | null>(null);
}
Cause Comuni di CLS e Soluzioni
| Causa | Impatto CLS | Soluzione |
|---|---|---|
| Immagini senza dimensioni | 0.1 - 0.5 | Attributi width/height o aspect-ratio CSS |
| Font web FOUT/FOIT | 0.05 - 0.15 | font-display: swap + size-adjust |
| Contenuto iniettato dinamicamente | 0.1 - 0.3 | Skeleton screen o min-height riservato |
| Banner cookie/notifiche | 0.05 - 0.2 | Overlay position fixed, non push content |
| Annunci e embed di terze parti | 0.1 - 0.4 | Contenitore con dimensioni riservate |
5. Ottimizzazioni Angular-Specifiche
Angular offre diverse funzionalità integrate che migliorano significativamente i Core Web
Vitals. La combinazione di SSR, lazy loading con @defer e route-based code
splitting crea un'architettura che carica solo il codice necessario, quando necessario.
SSR per LCP Ottimale
Come abbiamo visto nell'articolo precedente, il Server-Side Rendering invia HTML pre-renderizzato al browser, eliminando il tempo di attesa per il download e l'esecuzione di JavaScript. Questo ha un impatto diretto e misurabile sul LCP.
Lazy Loading con @defer
Il blocco @defer permette di caricare componenti on-demand, riducendo il
bundle JavaScript iniziale e migliorando sia LCP che INP.
<!-- Contenuto above-the-fold: caricato normalmente -->
<app-hero />
<app-featured-projects />
<!-- Below-the-fold: caricato quando visibile -->
@defer (on viewport) {
<app-blog-preview />
} @placeholder {
<app-blog-skeleton />
}
@defer (on viewport) {
<app-testimonials />
} @placeholder {
<div style="min-height: 400px;"></div>
}
<!-- Componenti pesanti: caricati su interazione -->
@defer (on interaction) {
<app-contact-form />
} @placeholder {
<button class="cta-button">Contattami</button>
}
<!-- Componenti non critici: caricati quando il browser e libero -->
@defer (on idle) {
<app-newsletter-signup />
}
Route-Based Code Splitting
// app.routes.ts - Code splitting automatico per route
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home-page').then(m => m.HomePageComponent),
},
{
path: 'blog',
loadComponent: () =>
import('./pages/blog/blog-page').then(m => m.BlogPageComponent),
},
{
path: 'blog/:slug',
loadComponent: () =>
import('./pages/article/article-page').then(m => m.ArticlePageComponent),
},
{
path: 'dev-tools',
loadChildren: () =>
import('./pages/dev-tools/dev-tools.routes').then(m => m.DEV_TOOLS_ROUTES),
},
];
Impatto del Code Splitting sulle Metriche
| Strategia | Bundle Iniziale | LCP | TTI |
|---|---|---|---|
| Nessun code splitting | 450KB | 3.2s | 4.1s |
| Route-based splitting | 180KB | 1.8s | 2.4s |
| Route + @defer splitting | 85KB | 0.9s | 1.3s |
| Route + @defer + SSR | 85KB | 0.6s | 1.1s |
6. Ottimizzazione del Bundle
La dimensione del bundle JavaScript è direttamente proporzionale al tempo di caricamento e parsing. Ridurre il bundle significa migliorare tutte le metriche: LCP (meno JS da scaricare), INP (meno codice da eseguire) e indirettamente CLS (rendering più rapido).
Analisi del Bundle con source-map-explorer
# Installa source-map-explorer
npm install --save-dev source-map-explorer
# Build di produzione con source maps
ng build --source-map
# Analizza il bundle principale
npx source-map-explorer dist/portfolio/browser/main-*.js
# Alternativa: usa webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/portfolio/browser/stats.json
Tree Shaking e Ottimizzazioni di Build
Angular CLI applica automaticamente tree shaking, minificazione e compressione nella build di produzione. Tuttavia, ci sono ulteriori ottimizzazioni che puoi configurare.
{
"projects": {
"portfolio": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "250kB",
"maximumError": "500kB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true
}
}
}
}
}
}
}
Dipendenze Pesanti da Controllare
Alcune librerie comuni aggiungono peso significativo al bundle. Verifica sempre
l'impatto con source-map-explorer e considera alternative più leggere:
- moment.js (~300KB): Sostituisci con
date-fns(~30KB tree-shaked) o API nativeIntl.DateTimeFormat - lodash (~70KB): Importa singole funzioni con
lodash-es/debounceo usa metodi nativi - rxjs (incluso): Importa solo gli operatori necessari, mai
import * from 'rxjs' - chart.js (~200KB): Registra solo i componenti necessari con
Chart.register() - @angular/material: Importa solo i moduli/componenti effettivamente usati
7. Misurare le Performance
Non puoi ottimizzare ciò che non misuri. Esistono diversi strumenti per misurare i Core Web Vitals, ognuno con vantaggi specifici. La strategia ideale combina dati di laboratorio (Lighthouse, DevTools) con dati reali degli utenti (RUM, CrUX).
La Libreria web-vitals
La libreria web-vitals di Google è il modo più semplice e affidabile per
misurare i Core Web Vitals nel browser. Si integra facilmente in un'applicazione Angular.
// web-vitals.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class WebVitalsService {
private platformId = inject(PLATFORM_ID);
async measureVitals(): Promise<void> {
if (!isPlatformBrowser(this.platformId)) return;
const { onLCP, onINP, onCLS, onFCP, onTTFB } = await import('web-vitals');
onLCP((metric) => {
this.reportMetric('LCP', metric.value, metric.rating);
});
onINP((metric) => {
this.reportMetric('INP', metric.value, metric.rating);
});
onCLS((metric) => {
this.reportMetric('CLS', metric.value, metric.rating);
});
onFCP((metric) => {
this.reportMetric('FCP', metric.value, metric.rating);
});
onTTFB((metric) => {
this.reportMetric('TTFB', metric.value, metric.rating);
});
}
private reportMetric(
name: string,
value: number,
rating: 'good' | 'needs-improvement' | 'poor'
): void {
// Invia a Google Analytics 4
if (typeof gtag !== 'undefined') {
gtag('event', name, {
value: Math.round(name === 'CLS' ? value * 1000 : value),
event_category: 'Web Vitals',
event_label: rating,
non_interaction: true,
});
}
// Log per debug in development
console.log(`[${rating.toUpperCase()}] ${name}: ${value.toFixed(2)}`);
}
}
Strumenti di Misurazione a Confronto
| Strumento | Tipo | Metriche | Utilizzo |
|---|---|---|---|
| Lighthouse | Lab data | LCP, CLS, TBT, FCP, SI | Audit durante sviluppo, CI/CD |
| Chrome DevTools Performance | Lab data | Tutte + flame chart | Debug dettagliato di long task |
| PageSpeed Insights | Lab + Field | CWV + suggerimenti | Overview rapida con dati CrUX |
| web-vitals (libreria) | Field data (RUM) | LCP, INP, CLS, FCP, TTFB | Monitoraggio real user in produzione |
| Chrome UX Report (CrUX) | Field data | CWV (dati aggregati reali) | Dati reali di utenti Chrome, usato per ranking |
| Search Console | Field data | CWV per URL | Impatto SEO e pagine problematiche |
8. Ottimizzazione Avanzata delle Immagini
Le immagini rappresentano tipicamente il 50-70% del peso totale di una pagina web. Un'ottimizzazione aggressiva delle immagini ha l'impatto più significativo sulle performance, specialmente su LCP e sul tempo di caricamento complessivo.
Formati Moderni: WebP e AVIF
I formati moderni offrono compressione superiore rispetto a JPEG e PNG senza perdita percepibile di qualità. AVIF in particolare offre risparmi del 50% rispetto a JPEG.
Responsive Images con srcset
// app.config.ts - Configurare un image loader
import { provideImageKitLoader } from '@angular/common';
// Oppure usa un loader personalizzato
import { IMAGE_LOADER, ImageLoaderConfig } from '@angular/common';
export const appConfig: ApplicationConfig = {
providers: [
// Opzione 1: Loader integrato (Cloudinary, ImageKit, Imgix)
provideImageKitLoader('https://ik.imagekit.io/myaccount'),
// Opzione 2: Loader personalizzato
{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
const width = config.width ? `?w=${config.width}` : '';
const quality = '&q=80';
const format = '&fm=webp';
return `https://cdn.example.com/${config.src}${width}${quality}${format}`;
}
}
]
};
// Utilizzo nel template
@Component({
template: `
<!-- Genera automaticamente srcset con dimensioni multiple -->
<img
ngSrc="hero-banner.jpg"
width="1200"
height="630"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 80vw, 1200px"
alt="Hero banner"
/>
`
})
export class HeroComponent {}
Confronto Formati Immagine
| Formato | Compressione vs JPEG | Supporto Browser | Uso Consigliato |
|---|---|---|---|
| JPEG | Baseline | 100% | Fallback per browser vecchi |
| WebP | -25-35% | 97%+ | Default per la maggior parte dei casi |
| AVIF | -50% | 92%+ | Fotografie, immagini complesse |
| PNG | +200-400% | 100% | Solo per trasparenza necessaria |
| SVG | N/A (vettoriale) | 100% | Icone, loghi, grafici semplici |
9. Ottimizzazione dei Font
I font web sono spesso una risorsa bloccante per il rendering: il browser non mostra il testo fino a quando il font non è stato scaricato (FOIT - Flash of Invisible Text) oppure mostra un font fallback che poi viene sostituito causando un layout shift (FOUT - Flash of Unstyled Text). Entrambi i comportamenti impattano CLS e LCP.
Strategia di Caricamento Font Ottimale
/* styles.css - Dichiarazione font ottimizzata */
/* Font con subset latino (riduce la dimensione del 60-70%) */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap; /* Mostra testo subito con fallback */
src: url('/assets/fonts/inter-v12-latin-regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
/* size-adjust riduce CLS causato dal cambio font */
size-adjust: 100%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/assets/fonts/inter-v12-latin-700.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/jetbrains-mono-v13-latin-regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
/* Font stack con fallback metricamente compatibili */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
code, pre {
font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code',
'SF Mono', Consolas, monospace;
}
Strategie font-display a Confronto
| Valore | Comportamento | Impatto CLS | Impatto LCP | Raccomandato |
|---|---|---|---|---|
swap |
Mostra fallback subito, scambia quando pronto | Medio (possibile FOUT) | Basso (testo visibile subito) | Si, con size-adjust |
block |
Nasconde testo per 3s, poi mostra fallback | Basso | Alto (FOIT, testo invisibile) | No |
fallback |
Nasconde 100ms, poi fallback per 3s | Basso-medio | Medio | Si, per font non critici |
optional |
Nasconde 100ms, usa solo se già in cache | Nessuno | Nessuno | Si, massima performance |
10. Monitoraggio in Produzione
I dati di laboratorio (Lighthouse) sono utili durante lo sviluppo, ma non rappresentano l'esperienza reale degli utenti. Il Real User Monitoring (RUM) raccoglie metriche dai browser reali degli utenti, fornendo dati rappresentativi delle condizioni effettive: device diversi, connessioni lente, geolocalizzazione variabile.
Performance Observer API
// performance-monitor.service.ts
@Injectable({ providedIn: 'root' })
export class PerformanceMonitorService {
private platformId = inject(PLATFORM_ID);
constructor() {
afterNextRender(() => {
this.observeLCP();
this.observeCLS();
this.observeLongTasks();
});
}
private observeLCP(): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as any;
const lcpValue = lastEntry.startTime;
const lcpElement = lastEntry.element?.tagName || 'unknown';
this.sendToAnalytics('LCP', {
value: Math.round(lcpValue),
element: lcpElement,
url: lastEntry.url || '',
rating: lcpValue <= 2500 ? 'good' : lcpValue <= 4000 ? 'needs-improvement' : 'poor'
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
private observeCLS(): void {
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as any[]) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
// Invia CLS quando l'utente lascia la pagina
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.sendToAnalytics('CLS', {
value: Math.round(clsScore * 1000),
rating: clsScore <= 0.1 ? 'good' : clsScore <= 0.25 ? 'needs-improvement' : 'poor'
});
}
});
}
private observeLongTasks(): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn(
`Long Task rilevato: ${entry.duration.toFixed(0)}ms`,
entry
);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
}
private sendToAnalytics(name: string, data: Record<string, any>): void {
// Invio a GA4
if (typeof gtag !== 'undefined') {
gtag('event', name, {
...data,
event_category: 'Web Vitals',
non_interaction: true,
});
}
// Invio a endpoint custom (beacon per affidabilità)
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', JSON.stringify({
metric: name,
...data,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
}));
}
}
}
Integrazione con Google Analytics 4
GA4 supporta nativamente i Core Web Vitals attraverso eventi personalizzati. Combinando
la libreria web-vitals con GA4 puoi monitorare le performance reali degli
utenti e creare dashboard per identificare pagine problematiche.
// Inizializzazione nel componente root
@Component({
selector: 'app-root',
standalone: true,
template: `<router-outlet />`
})
export class AppComponent {
private webVitals = inject(WebVitalsService);
private platformId = inject(PLATFORM_ID);
constructor() {
afterNextRender(() => {
// Avvia la misurazione dei Core Web Vitals
this.webVitals.measureVitals();
// Monitora le navigazioni SPA
this.trackNavigationTiming();
});
}
private trackNavigationTiming(): void {
const navigation = performance.getEntriesByType('navigation')[0] as any;
if (navigation) {
gtag('event', 'page_timing', {
dns_time: Math.round(navigation.domainLookupEnd - navigation.domainLookupStart),
tcp_time: Math.round(navigation.connectEnd - navigation.connectStart),
ttfb: Math.round(navigation.responseStart - navigation.requestStart),
dom_load: Math.round(navigation.domContentLoadedEventEnd - navigation.startTime),
full_load: Math.round(navigation.loadEventEnd - navigation.startTime),
});
}
}
}
Errori Comuni nel Monitoraggio delle Performance
- Fidarsi solo di Lighthouse: I punteggi di laboratorio non riflettono le condizioni reali. Usa sempre RUM insieme a Lighthouse
- Ignorare il 75° percentile: Google usa il 75° percentile (p75) dei dati di campo per il ranking. La mediana non basta
- Non segmentare i dati: Analizza le metriche per device (mobile vs desktop), connessione e geolocalizzazione
- Misurare solo la homepage: Ogni pagina ha metriche diverse. Le pagine con più contenuto (blog, prodotti) sono spesso le più problematiche
- Non usare sendBeacon: Le richieste
fetchpossono essere cancellate quando l'utente naviga via.navigator.sendBeacongarantisce l'invio - Non monitorare i Long Task: Long task > 50ms bloccano il main thread e peggiorano INP. Monitorali per identificare colli di bottiglia
Riepilogo e Prossimi Passi
Ottimizzare i Core Web Vitals in Angular non è un'operazione una tantum ma un processo continuo. Le metriche cambiano con ogni nuova funzionalità, ogni nuova dipendenza e ogni aggiornamento del framework. La chiave del successo è combinare misurazioni continue (RUM in produzione) con ottimizzazioni proattive (SSR, lazy loading, ottimizzazione immagini e font).
Concetti Chiave di Questo Articolo
- Core Web Vitals: LCP (≤ 2.5s), INP (≤ 200ms) e CLS (≤ 0.1) sono fattori di ranking Google
- LCP: Usa
NgOptimizedImageconpriority, SSR, preconnect e formati moderni (WebP/AVIF) - INP: Passa a Signals per eliminare change detection globale, suddividi i long task con
scheduler.yield() - CLS: Dimensioni esplicite su immagini/media, skeleton screen,
font-display: swapconsize-adjust - Bundle: Route-based code splitting +
@deferriducono il JS iniziale dell'80%+ - Immagini: WebP come default, AVIF per foto,
srcsetper responsive, lazy loading automatico - Font: Self-hosting, subset,
font-display: optionalper massima performance - Misurazione: Libreria
web-vitals+ GA4 per RUM, Lighthouse per lab data - Monitoraggio: Performance Observer API, Long Task detection,
sendBeaconper invio affidabile
Obiettivi di Performance Consigliati per Angular
| Metrica | Obiettivo | Come Raggiungerlo |
|---|---|---|
| LCP | < 1.5s | SSR + NgOptimizedImage + preload hero image |
| INP | < 100ms | Zoneless + Signals + event handler ottimizzati |
| CLS | < 0.05 | Dimensioni esplicite + skeleton + font-display optional |
| Lighthouse Performance | 90+ | Tutte le ottimizzazioni combinate |
| Bundle iniziale | < 100KB gzipped | Code splitting + tree shaking + @defer |
| TTFB | < 200ms | CDN + SSG per pagine statiche + edge caching |
Nel prossimo articolo della serie esploreremo le Angular PWA (Progressive Web App), analizzando come trasformare un'applicazione Angular in un'app installabile con supporto offline, notifiche push e aggiornamenti automatici tramite Service Worker.
Domande frequenti
Cosa sono i Core Web Vitals?
I Core Web Vitals sono un insieme di metriche definite da Google che misurano tre aspetti fondamentali dell'esperienza utente: velocità di caricamento, reattività alle interazioni e stabilità visiva.
Perché i Core Web Vitals sono importanti per Angular?
Le applicazioni Angular, specialmente quelle Single Page Application (SPA) con rendering lato client, hanno storicamente problemi con i Core Web Vitals a causa di fattori come il Server-Side Rendering, il caricamento delle immagini e le risorse bloccanti.
Come ottimizzare il Largest Contentful Paint (LCP)?
Per ottimizzare il LCP in Angular è possibile utilizzare la direttiva NgOptimizedImage per gestire lazy loading, priorità, dimensioni e formati moderni delle immagini. Inoltre, è consigliabile aggiungere la proprietà "loading" alle immagini per anticipare le operazioni di caricamento.
Come monitorare le performance dei Core Web Vitals?
Per monitorare le performance dei Core Web Vitals esistono diversi strumenti, tra cui la libreria web-vitals di Google, che si integra facilmente in un'applicazione Angular e fornisce dati rappresentativi delle condizioni effettive.

Commenti
Caricamento commenti...
Accedi per commentare