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 | Sinyallerle duyarlı formlar |
| 6 | SSR ve Artan Hidrasyon | Sunucu Tarafı İşleme |
| 7 | Buradasınız → Angular'da Önemli Web Verileri | Performans ve ölçümler |
| 8 | Açısal PWA | Aşamalı Web Uygulamaları |
| 9 | Gelişmiş Bağımlılık Ekleme | DI ağacı sarsılabilir |
| 10 | Angular 17'den 21'e geçiş | Geçiş kılavuzu |
1. Önemli Web Verileri Nelerdir?
Önemli Web Verileri, Google tarafından tanımlanan ve üç özelliği ölçen bir dizi ölçümdür Kullanıcı deneyiminin temelleri: yükleme hızı, etkileşimlere duyarlılık e görsel stabilite. 2021'den itibaren bunlar resmi olarak Google'ın sıralama sinyallerinin bir parçasıdır; bu, bir sitenin Temel Web Verileri zayıf olan içerikler arama sonuçlarında cezalandırılır.
Üç Temel Web Verisi Metrikleri
| Metrik | Neyi ölçer | İyi | Geliştirilmiş | Nadir |
|---|---|---|---|---|
| LCP (En Büyük İçerikli Boya) | Görünür en büyük öğeyi oluşturma zamanı | ≤ 2,5s | 2,5s - 4,0s | > 4.0s |
| INP (Sonraki Boyayla Etkileşim) | Kullanıcı etkileşimi ile görsel güncelleme arasındaki gecikme | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS (Kümülatif Düzen Kayması) | Sayfadaki öğelerin beklenmedik hareketi | ≤ 0,1 | 0,1 - 0,25 | > 0,25 |
FID'den INP'ye: Reaktivite Metriğinin Gelişimi
Mart 2024'te Google, İlk Giriş Gecikmesi (FID) ile Sonraki Boyayla Etkileşim (INP) resmi ölçü olarak. Fark temel olan, FID'nin yalnızca ölçtüğü gecikme ilk etkileşimin ardından INP gecikmeyi ölçer Tümü tüm oturum boyunca etkileşimler, en kötü değeri döndürüyor (98. yüzdelik dilimde). Bu, INP'yi bir metrik haline getirir gerçek kullanıcı deneyimini çok daha iyi temsil ediyor.
Angular İçin Neden Önemliler?
Açısal uygulamalar, özellikle işlemeli tek sayfalı uygulamalar (SPA'lar) istemci tarafının geçmişte Core Web Vitals'la ilgili sorunları var. Ana nedenler şunlardır:
- Ağır JavaScript Paketleri: Angular, çerçeveyi, bileşenleri ve bağımlılıkları tek bir başlangıç paketinde içerir
- Pahalı değişiklik tespiti: Zone.js her eşzamansız olayı yakalar ve değişiklik algılama döngülerini tetikler
- İstemci tarafı oluşturma: SSR olmadan, tarayıcının içeriği sunmadan önce tüm JS'yi indirmesi, ayrıştırması ve yürütmesi gerekir
- Tembel yükleme optimal değil: Yeterli stratejiler olmadığında, ilk girişte tüm kodlar yüklenir
2. En Büyük İçerikli Boyayı (LCP) Optimize Edin
Il LCP en büyük öğeyi oluşturmak için gereken süreyi ölçer ilk görünüm alanında görünür. Tipik olarak bu bir kahraman resmi, bir metin bloğu veya bir video öğesi. Angular için LCP, Sunucu Tarafı Oluşturma'dan büyük ölçüde etkilenir. görüntü yükleme ve kaynakların engellenmesinden.
NgOptimizedImage: Temel Yönerge
Angular direktifi sağlar NgOptimizedImage pakette
@angular/common için en iyi uygulamaları otomatik olarak uygulayan
görüntüleri yüklüyorum. Bu yönerge geç yüklemeyi, önceliği ve boyutu yönetir
ve modern formatlar.
// 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 {}
Yaygın Hata: Öncelik Özelliğini unutmak
Olmadan priority, NgOptimizedImage otomatik olarak uygula
loading="lazy" tüm resimlere. LCP görüntüsü için (tipik olarak kahraman
ekranın üst kısmındaki resim) bu ve verimsiz çünkü geciktiriyor
en önemli unsuru yüklüyoruz. Her zaman ekle priority için
LCP resminiz.
Kritik Kaynaklar için Ön Bağlantı ve Ön Yükleme
Tarayıcı indirmeden önce DNS'yi çözmeli, TCP bağlantıları kurmalı ve TLS ile anlaşmalıdır
harici alanlardan gelen kaynaklar. İle preconnect e preload yapabiliriz
Bu operasyonları önceden tahmin edin.
<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>
Angular için LCP Kontrol Listesi
- Amerika
NgOptimizedImageilepriorityLCP görüntüsünde - Önceden oluşturulmuş içerikle HTML göndermek için SSR'yi etkinleştirin
- Eklemek
preconnectCDN görüntüleri ve yazı tipleri için - Amerika
preloadkahraman görseli ve kritik yazı tipi için - Boyutu küçültmek için görüntüleri WebP veya AVIF'ye dönüştürün
- Otomatik ölçeklendirme için bir CDN görüntü yükleyicisi uygulayın
- Kritik kaynaklardaki yönlendirmelerden kaçının (her yönlendirme 300-500 ms ekler)
- Kritik CSS'yi küçültün ve HTML'ye satır içi ekleyin
3. Sonraki Boyayla Etkileşimi Optimize Edin (INP)
L'INP Kullanıcı etkileşimleri (tıklamalar, tıklamalar, dokunun, tuşa basın) ve ekrandaki bir sonraki görsel güncellemeyi gerçekleştirin. Açısal için bu metrik, değişiklik tespitinden, olay işleyicilerinden ve kullanımdan doğrudan etkilenir Zone.js'nin.
Değişiklik Tespitini Azaltacak Sinyaller
Angular'da INP için en etkili optimizasyonlardan biri Zone.js'den Zone.js'ye geçiştir. Sinyaller. Signals ile Angular yalnızca verileri olan bileşenleri günceller. aslında değişti ve küresel değişiklik tespit döngüleri ortadan kaldırıldı.
// 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);
}
}
Olay İşleyicilerinde Uzun Görevlerden Kaçının
Ağır hesaplamalar gerçekleştiren bir olay işleyicisi ana iş parçacığını bloke eder ve INP'yi kötüleştirir.
Strateji, işi mikro görevlere bölmektir. 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;
}
}
Zone.js'nin INP üzerindeki etkisi
| Senaryo | Zone.js ile | Bölgesiz (Sinyaller) | Gelişim |
|---|---|---|---|
| 100 öğelik listedeki düğmeye tıklayın | 85ms | 12ms | -86% |
| Arama alanına yazma | 120ms | 18ms | -85% |
| 1000 satırlı tabloda filtreyi değiştir | 250ms | 45ms | -82% |
| Sekmeler arasında gezinme | 65ms | 8ms | -88% |
4. Kümülatif Düzen Kaymasını (CLS) Önleyin
Il CLS elemanların beklenmeyen yer değiştirmelerinin toplamını ölçer. sayfa yaşam döngüsü boyunca. Yüksek CLS sinir bozucu bir deneyim yaratır: kullanıcı bir düğmeye tıklamak üzeredir ve düzen değişir, bu da onun başka bir şeye tıklamasına neden olur.
Görseller ve Medya için Açık Boyutlar
CLS'nin en yaygın nedeni, belirgin boyutları olmayan görüntüler ve iframe'lerdir. ne zaman tarayıcı görselin boyutunu bilmiyor, yüklemeye kadar sıfır alan ayırıyor, daha sonra düzen aniden yeniden hesaplanır.
<!-- 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>
Dinamik İçerik için İskelet Ekranı
İçerik eşzamansız olarak yüklendiğinde (API'den, veritabanından vb.), iskelet ekranı gerekli alanı korur ve düzenin değişmesini önler.
// 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);
}
CLS'nin Yaygın Nedenleri ve Çözümleri
| Neden | CLS etkisi | Çözüm |
|---|---|---|
| Boyutsuz görseller | 0,1 - 0,5 | Nitelikler width/height o aspect-ratio CSS |
| FOUT/FOIT web yazı tipleri | 0,05 - 0,15 | font-display: swap + size-adjust |
| İçerik dinamik olarak enjekte edildi | 0,1 - 0,3 | İskelet ekranı veya min-height Rezerve |
| Çerez/bildirim başlığı | 0,05 - 0,2 | Yer paylaşımı konumu sabit, içerik aktarılmıyor |
| Üçüncü taraf reklamları ve yerleştirmeleri | 0,1 - 0,4 | Ayrılmış boyutlara sahip konteyner |
5. Açıya Özel Optimizasyonlar
Angular, Core Web'i önemli ölçüde geliştiren çeşitli yerleşik özellikler sunar
Hayati bilgiler. SSR ve tembel yüklemenin kombinasyonu @defer ve rota tabanlı kod
bölme, gerektiğinde yalnızca gerekli kodu yükleyen bir mimari oluşturur.
Optimum LCP için SSR
Önceki makalede gördüğümüz gibi, Sunucu Tarafı Oluşturma, HTML gönderir tarayıcıya önceden işlenerek indirme ve çalıştırma için bekleme süresini ortadan kaldırır JavaScript'in. Bunun PCL üzerinde doğrudan ve ölçülebilir bir etkisi vardır.
@defer ile Tembel Yükleniyor
Blok @defer bileşenleri isteğe bağlı olarak yüklemenize olanak tanır ve
Başlangıç JavaScript paketi ve hem LCP hem de INP'nin iyileştirilmesi.
<!-- 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 />
}
Rota Tabanlı Kod Bölme
// 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),
},
];
Kod Bölmenin Metrikler Üzerindeki Etkisi
| Strateji | İlk Paket | LCP | TTI |
|---|---|---|---|
| Kod bölme yok | 450KB | 3.2s | 4.1'ler |
| Rota tabanlı bölme | 180KB | 1.8s | 2,4 saniye |
| Rota + @erteleme bölme | 85KB | 0,9s | 1,3 saniye |
| Rota + @ertele + SSR | 85KB | 0,6 saniye | 1.1s |
6. Paket optimizasyonu
JavaScript paketinin boyutu yükleme süresiyle doğru orantılıdır ve ayrıştırma. Paketin azaltılması tüm metriklerin iyileştirilmesi anlamına gelir: LCP (daha az JS) indirme), INP (yürütülecek daha az kod) ve dolaylı olarak CLS (daha hızlı oluşturma).
Kaynak harita gezgini ile paket analizi
# 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
Ağaç Sallama ve Yapı Optimizasyonları
Angular CLI, yapıya otomatik olarak ağaç sallama, küçültme ve sıkıştırma uygular üretim. Ancak yapılandırabileceğiniz ek optimizasyonlar vardır.
{
"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
}
}
}
}
}
}
}
Kontrol Edilecek Ağır Bağımlılıklar
Bazı yaygın kitaplıklar pakete önemli ölçüde ağırlık katar. Her zaman kontrol et
olan etki source-map-explorer ve daha hafif alternatifleri değerlendirin:
- moment.js (~300KB): Şununla değiştirin:
date-fns(~30KB ağaç sallamalı) veya yerel APIIntl.DateTimeFormat - lodash (~70KB): Tek tek işlevleri içe aktarın
lodash-es/debounceveya yerel yöntemleri kullanın - rxjs (dahil): Yalnızca gerekli operatörleri içe aktarın, asla
import * from 'rxjs' - chart.js (~200KB): Yalnızca gerekli bileşenleri kaydedin
Chart.register() - @açısal/malzeme: Yalnızca gerçekten kullanılan modülleri/bileşenleri içe aktarın
7. Performansı Ölçün
Ölçmediğiniz şeyi optimize edemezsiniz. Çekirdekleri ölçmek için çeşitli araçlar vardır Web Verileri'nin her birinin kendine özgü avantajları vardır. İdeal strateji laboratuvar verilerini birleştirir (Lighthouse, DevTools) gerçek kullanıcı verileriyle (RUM, CrUX).
Web hayati kütüphanesi
Kütüphane web-vitals Google'dan en kolay ve en güvenilir yol
Tarayıcıda Önemli Web Verilerini ölçün. Angular uygulamasına kolayca entegre olur.
// 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)}`);
}
}
Ölçüm araçlarının karşılaştırılması
| Enstrüman | Tip | Metrikler | Kullanım |
|---|---|---|---|
| Deniz feneri | Laboratuvar verileri | LCP, CLS, TBT, FCP, SI | Geliştirme sırasında denetim, CI/CD |
| Chrome Geliştirici Araçları Performansı | Laboratuvar verileri | Hepsi + alev grafiği | Uzun görevlerin ayrıntılı hata ayıklaması |
| Sayfa Hızı Analizleri | Laboratuvar + Saha | CWV + ipuçları | CrUX verileriyle hızlı genel bakış |
| web hayati bilgileri (kütüphane) | Saha verileri (RUM) | LCP, INP, CLS, FCP, TTFB | Üretimde gerçek kullanıcı izleme |
| Chrome Kullanıcı Deneyimi Raporu (CrUX) | Saha verileri | CWV (gerçek toplu veriler) | Sıralama için kullanılan Chrome kullanıcılarının gerçek verileri |
| Arama Konsolu | Saha verileri | URL için CWV | SEO etkisi ve sorunlu sayfalar |
8. Gelişmiş Görüntü Optimizasyonu
Resimler genellikle bir web sayfasının toplam ağırlığının %50-70'ini temsil eder. Agresif görüntü optimizasyonu en önemli etkiye sahiptir özellikle LCP ve genel yükleme süresi açısından performans.
Modern formatlar: WebP ve AVIF
Modern formatlar, JPEG ve PNG'den daha üstün kayıpsız sıkıştırma sunar algılanabilir kalite. AVIF özellikle JPEG'e kıyasla %50 oranında tasarruf sağlar.
Srcset ile Duyarlı Görüntüler
// 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 {}
Görüntü Formatları Karşılaştırması
| Biçim | Sıkıştırma ve JPEG | Tarayıcı Desteği | Önerilen Kullanım |
|---|---|---|---|
| JPEG | Temel çizgiler | %100 | Eski tarayıcılar için geri dönüş |
| WebP | -25-35% | %97+ | Çoğu durumda varsayılan |
| AVIF | -%50 | %92+ | Fotoğraflar, karmaşık görüntüler |
| NPC'ler | +%200-400 | %100 | Yalnızca gerekli şeffaflık için |
| SVG | Yok (vektör) | %100 | Basit simgeler, logolar, grafikler |
9. Yazı Tipi Optimizasyonu
Web yazı tipleri genellikle oluşturmayı engelleyen bir kaynaktır: tarayıcı, yazı tipi indirilene kadar metin (FOIT - Görünmez Metin Flash) veya daha sonra değiştirilen ve düzen değişikliğine neden olan bir yedek yazı tipini gösterir (FOUT - Flash of Biçimlendirilmemiş Metin). Her iki davranış da CLS ve LCP'yi etkiler.
Optimum Yazı Tipi Yükleme Stratejisi
/* 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;
}
Yazı tipi görüntüleme stratejilerini karşılaştırma
| Değer | Davranış | CLS etkisi | LCP etkisi | Tavsiye edilen |
|---|---|---|---|---|
swap |
Geri dönüşü hemen göster, hazır olduğunda değiştir | Orta (mümkün FOUT) | Düşük (metin hemen görünür) | Evet, boyut ayarlamalı |
block |
Metni 3 saniye gizleyin, ardından yedeği gösterin | Bas | Yüksek (FOIT, görünmez metin) | No |
fallback |
100 ms'yi gizler, ardından 3 saniyeliğine geri döner | Düşük-orta | Orta | Evet, kritik olmayan yazı tipleri için |
optional |
100ms'yi gizle, yalnızca zaten önbellekteyse kullan | Hiç kimse | Hiç kimse | Evet, maksimum performans |
10. Üretim Takibi
Laboratuvar (Deniz Feneri) verileri geliştirme sırasında faydalıdır ancak temsil etmez gerçek kullanıcı deneyimi. Gerçek Kullanıcı İzleme (RUM) toplar Koşulları temsil eden verileri sağlayan, kullanıcıların gerçek tarayıcılarından alınan ölçümler etkili: farklı cihazlar, yavaş bağlantılar, değişken coğrafi konum.
Performans Gözlemcisi API'si
// 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,
}));
}
}
}
Google Analytics 4 ile entegrasyon
GA4, özel etkinlikler aracılığıyla Temel Web Verilerini yerel olarak destekler. Birleştirme
kütüphane web-vitals GA4 ile gerçek performansı izleyebilirsiniz
kullanıcılar ve sorunlu sayfaları belirlemek için kontrol panelleri oluşturun.
// 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),
});
}
}
}
Performans İzlemede Yaygın Hatalar
- Yalnızca Deniz Feneri'ne güvenin: Laboratuvar puanları gerçek koşulları yansıtmamaktadır. RUM'u her zaman Lighthouse ile birlikte kullanın
- 75. yüzdelik dilim göz ardı edilirse: Google, sıralama için saha verilerinin 75. yüzdelik dilimini (p75) kullanır. Medyan yeterli değil
- Verilerinizi bölümlere ayırmayın: Ölçümleri cihaza (mobil ve masaüstü), bağlantıya ve coğrafi konuma göre analiz edin
- Yalnızca ana sayfayı ölçün: Her sayfanın farklı ölçümleri vardır. En fazla içeriğe sahip sayfalar (bloglar, ürünler) genellikle en sorunlu sayfalardır
- sendBeacon'u kullanmayın: istekler
fetchkullanıcı başka bir yere göz attığında silinebilirler.navigator.sendBeaconteslimatı garanti eder - Uzun Görevleri izlemeyin: 50 ms'den uzun görevler ana iş parçacığını bloke eder ve INP'yi kötüleştirir. Darboğazları belirlemek için bunları izleyin
Özet ve Sonraki Adımlar
Önemli Web Verilerini Angular'da optimize etmek tek seferlik bir işlem değil, bir süreçtir sürekli. Metrikler her yeni özellik, her yeni bağımlılık ve her yeni bağımlılıkla birlikte değişir. çerçeve güncellemesi. Başarının anahtarı birleştirmektir ölçümler devam et (Üretimdeki RUM) ile proaktif optimizasyonlar (SSR, tembel yükleme, resim ve yazı tipi optimizasyonu).
Bu Makalenin Temel Kavramları
- Önemli Web Verileri: LCP (≤ 2,5s), INP (≤ 200ms) ve CLS (≤ 0,1) Google sıralama faktörleridir
- LCP: Amerika
NgOptimizedImageilepriority, SSR, ön bağlantı ve modern formatlar (WebP/AVIF) - Giriş: Küresel değişiklik tespitini ortadan kaldırmak için Sinyallere geçin, uzun görevleri
scheduler.yield() - CLS: Görüntülerde/medyada, iskelet ekranında açık boyutlar,
font-display: swapilesize-adjust - Paketler: Rota tabanlı kod bölme +
@deferBaşlangıçtaki JS'yi %80'den fazla azaltın - Görseller: Varsayılan olarak WebP, fotoğraflar için AVIF,
srcsetduyarlı, otomatik tembel yükleme için - Yazı tipi: Kendi kendine barındırma, alt küme,
font-display: optionalmaksimum performans için - Ölçüm: Kitaplık
web-vitals+ RUM için GA4, laboratuvar verileri için Lighthouse - İzleme: Performance Observer API'si, Uzun Görev tespiti,
sendBeacongüvenilir gönderim için
Angular için Önerilen Performans Hedefleri
| Metrik | Amaç | Ona nasıl ulaşılır |
|---|---|---|
| LCP | < 1,5sn | SSR + NgOptimizedImage + kahraman resmini önceden yükleme |
| INP | < 100ms | Bölgesiz + Sinyaller + optimize edilmiş olay işleyicileri |
| CLS | < 0,05 | Açık boyutlar + iskelet + yazı tipi ekranı isteğe bağlı |
| Deniz Feneri Performansı | 90+ | Tüm optimizasyonlar birleştirildi |
| İlk paket | < 100 KB gzip'li | Kod bölme + ağaç sallama + @defer |
| TTFB | < 200ms | Statik sayfalar için CDN + SSG + uç önbelleğe alma |
Serinin bir sonraki makalesinde bu konuyu inceleyeceğiz. Açısal PWA (Aşamalı Web Uygulaması), Angular uygulamasının destekle kurulabilir bir uygulamaya nasıl dönüştürüleceğini analiz etmek Service Worker aracılığıyla çevrimdışı, anlık bildirimler ve otomatik güncellemeler.







