Angular에서 핵심 웹 바이탈 최적화
I 핵심 웹 바이탈 Google이 사용하는 성능 측정항목입니다. 웹사이트 사용자 경험의 품질을 결정하는 순위 요소입니다. 그들을 위해 Angular 애플리케이션에서 이러한 측정항목을 최적화하는 것은 단순한 기술적인 문제가 아닙니다. 검색 엔진의 가시성과 사용자 만족도에 직접 투자합니다.
이 시리즈의 일곱 번째 기사에서는 현대적인 각도 우리는 깊이 탐구할 것입니다
핵심 웹 바이탈(Core Web Vital)이 무엇인지, 이를 측정하는 방법, 그리고 무엇보다도 애플리케이션에서 이를 최적화하는 방법
LCP, INP 및 CLS에 대한 특정 기술을 갖춘 Angular입니다. 이미지 관리부터
NgOptimizedImage 번들을 줄이기 위해 @defer, 까지
실제 사용자 모니터링을 통해 프로덕션 환경에서 모니터링합니다.
이 기사에서 배울 내용
- 핵심 웹 바이탈(Core Web Vital)이란 무엇이며 왜 Google 순위 요소인가요?
- NgOptimizedImage 및 사전 연결을 사용하여 LCP(Largest Contentful Paint)를 최적화하는 방법
- 신호를 사용하여 INP(Next Paint)와의 상호 작용을 개선하고 변경 감지를 줄이는 전략
- 명시적 치수 및 스켈레톤 화면으로 CLS(누적 레이아웃 변경)를 방지하는 방법
- 각도별 최적화: SSR, 지연 로딩
@defer, 경로 기반 코드 분할 - 트리 쉐이킹, 축소 및 소스 맵 탐색기를 사용한 번들 분석 및 축소
- 웹 바이탈, Lighthouse 및 Chrome DevTools를 사용한 성능 측정
- 웹용 이미지 및 글꼴의 고급 최적화
- Performance Observer API 및 GA4를 사용한 프로덕션 모니터링
최신 Angular 시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 각도 신호 | 세분화된 응답성 |
| 2 | 영역 없는 변경 감지 | Zone.js 삭제 |
| 3 | 새로운 템플릿 @if, @for, @defer | 현대적인 제어 흐름 |
| 4 | 독립형 구성 요소 | NgModule이 없는 아키텍처 |
| 5 | 신호 형태 | 신호가 포함된 반응형 양식 |
| 6 | SSR 및 증분 수화 | 서버 측 렌더링 |
| 7 | 현재 위치 → Angular의 핵심 웹 바이탈 | 성능 및 지표 |
| 8 | 각도 PWA | 프로그레시브 웹 앱 |
| 9 | 고급 종속성 주입 | DI 트리 셰이크 가능 |
| 10 | Angular 17에서 21로 마이그레이션 | 마이그레이션 가이드 |
1. 핵심 웹 바이탈이란 무엇입니까?
핵심 웹 바이탈은 Google에서 정의한 세 가지 측면을 측정하는 측정항목 집합입니다. 사용자 경험의 기본: 로딩 속도, 상호작용에 대한 반응성 e 시각적 안정성. 2021년부터 이는 공식적으로 Google 순위 신호의 일부입니다. 코어 웹 바이탈이 좋지 않은 경우 검색 결과에서 불이익을 받습니다.
세 가지 핵심 웹 바이탈 지표
| 미터법 | 측정 대상 | 좋은 | 개선됨 | 희귀한 |
|---|---|---|---|---|
| LCP (콘텐츠가 포함된 최대 페인트) | 가장 큰 가시 요소를 렌더링하는 데 걸리는 시간 | 2.5초 이하 | 2.5초 - 4.0초 | > 4.0초 |
| INP (다음 페인트와의 상호작용) | 사용자 상호 작용과 시각적 업데이트 간의 지연 시간 | 200ms 이하 | 200ms - 500ms | > 500ms |
| CLS (누적 레이아웃 이동) | 페이지에서 요소가 예기치 않게 이동함 | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
FID에서 INP로: 반응성 측정법의 진화
2024년 3월 Google은 첫 번째 입력 지연(FID) ~와 함께 다음 페인트(INP)와의 상호작용 공식 지표로 사용됩니다. 차이점 근본적인 것은 FID가 단지 지연 첫 번째 상호작용 동안 INP는 대기 시간을 측정합니다. 모두 전체 세션 동안의 상호 작용, 최악의 값(98번째 백분위수)을 반환합니다. 이는 INP를 측정항목으로 만듭니다. 실제 사용자 경험을 훨씬 더 잘 나타냅니다.
Angular에 중요한 이유
Angular 애플리케이션, 특히 렌더링이 포함된 단일 페이지 애플리케이션(SPA) 클라이언트 측에서는 역사적으로 Core Web Vitals에 문제가 있었습니다. 주요 이유는 다음과 같습니다.
- 무거운 JavaScript 번들: Angular는 하나의 초기 번들에 프레임워크, 구성 요소 및 종속성을 포함합니다.
- 비용이 많이 드는 변경 감지: Zone.js는 모든 비동기 이벤트를 가로채고 변경 감지 주기를 트리거합니다.
- 클라이언트측 렌더링: SSR이 없으면 브라우저는 콘텐츠를 제공하기 전에 모든 JS를 다운로드, 구문 분석 및 실행해야 합니다.
- 지연 로딩이 최적이 아님: 적절한 전략이 없으면 첫 번째 로그인 시 모든 코드가 로드됩니다.
2. 콘텐츠가 포함된 최대 페인트(LCP) 최적화
Il LCP 가장 큰 요소를 렌더링하는 데 걸리는 시간을 측정합니다. 초기 뷰포트에 표시됩니다. 일반적으로 이는 히어로 이미지, 텍스트 블록 또는 비디오 요소. Angular의 경우 LCP는 서버 측 렌더링의 영향을 많이 받습니다. 이미지 로딩 및 차단 리소스에서.
NgOptimizedImage: 기본 지침
Angular는 지시어를 제공합니다 NgOptimizedImage 패키지에
@angular/common 다음에 대한 모범 사례를 자동으로 적용합니다.
이미지 업로드 중. 이 지시문은 지연 로딩, 우선순위, 크기를 관리합니다.
그리고 현대적인 형식.
// 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 {}
일반적인 실수: 우선순위 속성을 잊어버림
없이 priority, NgOptimizedImage 자동으로 적용
loading="lazy" 모든 이미지에. LCP 이미지의 경우(일반적으로 영웅
스크롤 없이 볼 수 있는 이미지) 이 및 비생산적인 시간을 지연시키기 때문에
가장 중요한 요소를 로드합니다. 항상 추가 priority 에
LCP 이미지.
중요 리소스에 대한 사전 연결 및 사전 로드
브라우저는 다운로드하기 전에 DNS를 확인하고, TCP 연결을 설정하고, TLS를 협상해야 합니다.
외부 도메인의 리소스. 와 함께 preconnect e preload 우리는 할 수 있다
이러한 작업을 예상하십시오.
<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용 LCP 체크리스트
- 미국
NgOptimizedImage~와 함께priorityLCP 이미지에서 - 사전 렌더링된 콘텐츠가 포함된 HTML을 보내려면 SSR을 활성화하세요.
- 추가하다
preconnectCDN 이미지 및 글꼴용 - 미국
preload히어로 이미지와 중요한 글꼴의 경우 - 이미지를 WebP 또는 AVIF로 변환하여 크기 줄이기
- 자동 크기 조정을 위한 CDN 이미지 로더 구현
- 중요한 리소스에 대한 리디렉션 방지(각 리디렉션마다 300~500ms 추가)
- 중요한 CSS를 축소하고 HTML에 인라인하세요.
3. 다음 페인트(INP)에 대한 상호작용 최적화
L'INP 사용자 상호작용(클릭, 탭, 키 누르기) 및 화면의 다음 시각적 업데이트. 각도의 경우 측정항목은 변경 감지, 이벤트 핸들러 및 사용량에 직접적인 영향을 받습니다. Zone.js의
변경 감지를 줄이기 위한 신호
Angular의 INP에 대한 가장 영향력 있는 최적화 중 하나는 Zone.js에서 신호. 신호를 사용하면 Angular는 데이터가 있는 구성 요소만 업데이트합니다. 실제로 변경되어 글로벌 변경 감지 주기가 제거됩니다.
// 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);
}
}
이벤트 처리기에서 긴 작업 방지
과도한 계산을 수행하는 이벤트 핸들러는 메인 스레드를 차단하고 INP를 악화시킵니다.
전략은 다음을 사용하여 작업을 마이크로 작업으로 나누는 것입니다. 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가 INP에 미치는 영향
| 대본 | Zone.js 사용 | 존리스(신호) | 개선 |
|---|---|---|---|
| 100개 항목 목록에서 버튼을 클릭하세요. | 85ms | 12ms | -86% |
| 검색창에 입력하기 | 120ms | 18ms | -85% |
| 1000행 테이블에서 필터 전환 | 250ms | 45ms | -82% |
| 탭 간 탐색 | 65ms | 8ms | -88% |
4. CLS(누적 레이아웃 이동) 방지
Il CLS 요소의 예상치 못한 변위의 합을 측정합니다. 수명주기 동안 페이지. 높은 CLS는 사용자에게 실망스러운 경험을 선사합니다. 그가 버튼을 클릭하려고 하는데 레이아웃이 바뀌면서 다른 것을 클릭하게 됩니다.
이미지 및 미디어의 명시적 크기
CLS의 가장 일반적인 원인은 명시적인 크기가 없는 이미지와 iframe입니다. 때 브라우저는 이미지의 크기를 모르고 로드될 때까지 공간을 할당하지 않습니다. 그런 다음 레이아웃이 갑자기 다시 계산됩니다.
<!-- 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>
동적 콘텐츠의 뼈대 화면
콘텐츠가 비동기적으로(API, 데이터베이스 등에서) 로드되면 스켈레톤 스크린 필요한 공간을 보존하고 레이아웃 변경을 방지합니다.
// 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의 일반적인 원인 및 해결 방법
| 원인 | CLS 영향 | 해결책 |
|---|---|---|
| 크기가 없는 이미지 | 0.1 - 0.5 | 속성 width/height o aspect-ratio CSS |
| FOUT/FOIT 웹 글꼴 | 0.05 - 0.15 | font-display: swap + size-adjust |
| 동적으로 삽입된 콘텐츠 | 0.1 - 0.3 | 뼈대 화면 또는 min-height 예약된 |
| 쿠키/알림 배너 | 0.05 - 0.2 | 오버레이 위치가 고정되었으며 콘텐츠를 푸시하지 않음 |
| 제3자 광고 및 삽입 | 0.1 - 0.4 | 예약된 크기가 있는 컨테이너 |
5. 각도별 최적화
Angular는 Core Web을 크게 향상시키는 여러 내장 기능을 제공합니다.
바이탈. SSR과 지연 로딩의 조합 @defer 및 경로 기반 코드
분할은 필요한 경우 필요한 코드만 로드하는 아키텍처를 생성합니다.
최적의 LCP를 위한 SSR
이전 기사에서 보았듯이 서버 측 렌더링은 HTML을 보냅니다. 브라우저에 사전 렌더링되므로 다운로드 및 실행에 소요되는 대기 시간이 사라집니다. 자바스크립트의. 이는 PCL에 직접적이고 측정 가능한 영향을 미칩니다.
@defer를 사용한 지연 로딩
블록 @defer 필요에 따라 구성 요소를 로드할 수 있으므로
초기 JavaScript 번들이며 LCP와 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 />
}
경로 기반 코드 분할
// 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),
},
];
코드 분할이 지표에 미치는 영향
| 전략 | 초기 번들 | LCP | TTI |
|---|---|---|---|
| 코드 분할 없음 | 450KB | 3.2초 | 4.1초 |
| 경로 기반 분할 | 180KB | 1.8초 | 2.4초 |
| Route + @defer 분할 | 85KB | 0.9초 | 1.3초 |
| 경로 + @defer + SSR | 85KB | 0.6초 | 1.1초 |
6. 번들 최적화
JavaScript 번들의 크기는 로드 시간에 정비례합니다. 그리고 파싱. 번들을 줄인다는 것은 모든 지표가 개선된다는 의미입니다. LCP(JS가 적음) 다운로드), INP(실행할 코드 적음) 및 간접적인 CLS(더 빠른 렌더링).
소스 맵 탐색기를 사용한 번들 분석
# 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
트리 쉐이킹 및 빌드 최적화
Angular CLI는 빌드에서 트리 쉐이킹, 축소 및 압축을 자동으로 적용합니다. 생산의. 그러나 구성할 수 있는 추가 최적화가 있습니다.
{
"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
}
}
}
}
}
}
}
통제에 대한 과도한 중독
일부 공통 라이브러리는 번들에 상당한 무게를 추가합니다. 항상 확인하세요
에 미치는 영향 source-map-explorer 더 가벼운 대안을 고려하십시오.
- 순간.js (~300KB): 다음으로 교체
date-fns(~30KB 트리 쉐이크) 또는 기본 APIIntl.DateTimeFormat - 로다시 (~70KB): 다음을 사용하여 개별 기능을 가져옵니다.
lodash-es/debounce또는 기본 방법을 사용하십시오 - rxjs (포함): 필요한 연산자만 가져오고 가져오지 않음
import * from 'rxjs' - 차트.js (~200KB) : 필요한 구성요소만 등록
Chart.register() - @각도/재료: 실제로 사용되는 모듈/컴포넌트만 Import
7. 성과 측정
측정하지 않는 것은 최적화할 수 없습니다. 코어 측정을 위한 여러 도구가 있습니다. 웹 바이탈은 각각 특정한 이점을 제공합니다. 실험실 데이터를 결합하는 이상적인 전략 (Lighthouse, DevTools)와 실제 사용자 데이터(RUM, CrUX).
웹 바이탈 라이브러리
도서관 web-vitals Google에서 제공하는 가장 쉽고 안정적인 방법입니다.
브라우저에서 핵심 웹 바이탈을 측정합니다. 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)}`);
}
}
측정 도구 비교
| 기구 | 유형 | 측정항목 | 용법 |
|---|---|---|---|
| 등대 | 실험실 데이터 | LCP, CLS, TBT, FCP, SI | 개발 중 감사, CI/CD |
| Chrome DevTools 성능 | 실험실 데이터 | 모두 + 플레임 차트 | 긴 작업의 상세한 디버깅 |
| PageSpeed 인사이트 | 연구실 + 현장 | CWV + 팁 | CrUX 데이터에 대한 간략한 개요 |
| 웹 바이탈(라이브러리) | 필드 데이터(RUM) | LCP, INP, CLS, FCP, TTFB | 프로덕션 환경의 실제 사용자 모니터링 |
| 크롬 UX 보고서(CrUX) | 현장 데이터 | CWV(실제 집계 데이터) | 순위에 사용되는 Chrome 사용자의 실제 데이터 |
| 검색 콘솔 | 현장 데이터 | URL에 대한 CWV | SEO 영향 및 문제가 있는 페이지 |
8. 고급 이미지 최적화
이미지는 일반적으로 웹페이지 전체 무게의 50~70%를 차지합니다. 공격적인 이미지 최적화는 다음에 가장 큰 영향을 미칩니다. 특히 LCP 및 전체 로드 시간에 대한 성능이 향상되었습니다.
최신 형식: WebP 및 AVIF
최신 형식은 JPEG 및 PNG보다 우수한 무손실 압축을 제공합니다. 인지되는 품질. 특히 AVIF는 JPEG에 비해 50%의 비용 절감 효과를 제공합니다.
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 {}
이미지 형식 비교
| 체재 | 압축과 JPEG | 브라우저 지원 | 권장 용도 |
|---|---|---|---|
| JPEG | 기준선 | 100% | 이전 브라우저에 대한 대체 |
| 웹P | -25-35% | 97%+ | 대부분의 경우 기본값 |
| AVIF | -50% | 92%+ | 사진, 복잡한 이미지 |
| NPC | +200-400% | 100% | 필요한 투명성을 위해서만 |
| SVG | 해당 없음(벡터) | 100% | 간단한 아이콘, 로고, 그래픽 |
9. 글꼴 최적화
웹 글꼴은 종종 렌더링을 차단하는 리소스입니다. 브라우저는 글꼴이 다운로드될 때까지 텍스트(FOIT - Flash of Invisible Text) 또는 레이아웃 변경을 유발하는 대체 글꼴을 보여줍니다(FOUT - Flash of 스타일이 지정되지 않은 텍스트). 두 동작 모두 CLS 및 LCP에 영향을 미칩니다.
최적의 글꼴 로딩 전략
/* 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;
}
글꼴 표시 전략 비교
| Valore | 행동 | CLS 영향 | LCP 영향 | 추천 |
|---|---|---|---|---|
swap |
즉시 대체 표시, 준비되면 교체 | 중간(FOUT 가능) | 낮음(텍스트가 즉시 표시됨) | 예, 크기 조정 가능 |
block |
3초 동안 텍스트를 숨긴 후 대체 표시 | 베이스 | 높음(FOIT, 보이지 않는 텍스트) | No |
fallback |
100ms를 숨긴 후 3초 동안 대체 | 낮음-중간 | 중간 | 예, 중요하지 않은 글꼴의 경우 |
optional |
100ms 숨기기, 이미 캐시에 있는 경우에만 사용 | 아무도 | 아무도 | 예, 최대 성능 |
10. 생산 모니터링
실험실(Lighthouse) 데이터는 개발 중에 유용하지만 데이터를 대표하지는 않습니다. 실제 사용자 경험. 그만큼 실제 사용자 모니터링(RUM) 수집하다 사용자의 실제 브라우저에서 얻은 측정항목을 통해 조건을 나타내는 데이터 제공 효과적: 다양한 장치, 느린 연결, 다양한 지리적 위치.
성능 관찰자 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,
}));
}
}
}
Google Analytics 4와 통합
GA4는 기본적으로 맞춤 이벤트를 통해 핵심 웹 바이탈을 지원합니다. 결합
도서관 web-vitals GA4를 사용하면
문제가 있는 페이지를 식별하기 위해 사용자를 관리하고 대시보드를 생성합니다.
// 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),
});
}
}
}
성능 모니터링의 일반적인 실수
- 등대만 신뢰하세요: 실험실 점수는 실제 조건을 반영하지 않습니다. 항상 Lighthouse와 함께 RUM을 사용하십시오.
- 75번째 백분위수 무시: Google은 순위 지정을 위해 필드 데이터의 75번째 백분위수(p75)를 사용합니다. 중앙값이 충분하지 않습니다.
- 데이터를 분류하지 마세요. 장치(모바일 대 데스크톱), 연결 및 지리적 위치별로 측정항목을 분석합니다.
- 홈페이지만 측정: 각 페이지에는 서로 다른 측정항목이 있습니다. 콘텐츠(블로그, 제품)가 가장 많은 페이지가 가장 문제가 되는 경우가 많습니다.
- sendBeacon을 사용하지 마세요: 요청
fetch사용자가 탐색할 때 삭제될 수 있습니다.navigator.sendBeacon배송을 보장합니다 - 장기 작업을 모니터링하지 마세요. 50ms를 초과하는 긴 작업은 메인 스레드를 차단하고 INP를 악화시킵니다. 병목 현상을 식별하기 위해 모니터링
요약 및 다음 단계
Angular에서 핵심 웹 바이탈 최적화는 일회성 작업이 아닌 프로세스입니다. 계속. 측정항목은 각각의 새로운 기능, 각각의 새로운 종속성 및 각각에 따라 변경됩니다. 프레임워크 업데이트. 성공의 열쇠는 결합이다 측정 계속하다 (생산 중인 RUM) 사전 최적화 (SSR, 지연 로딩, 이미지 및 글꼴 최적화).
이 기사의 주요 개념
- 핵심 웹 바이탈: LCP(< 2.5s), INP(< 200ms) 및 CLS(< 0.1)는 Google 순위 요소입니다.
- LCP: 미국
NgOptimizedImage~와 함께priority, SSR, 사전 연결 및 최신 형식(WebP/AVIF) - INP: 전역 변경 감지를 제거하기 위해 신호로 전환하고 긴 작업을 다음과 같이 분할합니다.
scheduler.yield() - CLS: 이미지/미디어의 명시적 크기, 스켈레톤 화면,
font-display: swap~와 함께size-adjust - 번들: 경로 기반 코드 분할 +
@defer초기 JS를 80% 이상 줄입니다. - 이미지: WebP는 기본, 사진은 AVIF,
srcset반응형 자동 지연 로딩을 위해 - 세례반: 자체 호스팅, 하위 집합,
font-display: optional최대 성능을 위해 - 측정: 책장
web-vitals+ RUM용 GA4, 실험실 데이터용 Lighthouse - 모니터링: Performance Observer API, 장기 작업 감지,
sendBeacon안정적인 발송을 위해
Angular에 권장되는 성능 목표
| 미터법 | 목적 | 도달하는 방법 |
|---|---|---|
| LCP | 1.5초 미만 | SSR + NgOptimizedImage + 히어로 이미지 사전 로드 |
| INP | < 100ms | Zoneless + 신호 + 최적화된 이벤트 핸들러 |
| CLS | < 0.05 | 명시적 치수 + 뼈대 + 글꼴 표시 옵션 |
| 등대 공연 | 90세 이상 | 모든 최적화가 결합됨 |
| 초기 번들 | < 100KB gzip으로 압축됨 | 코드 분할 + 트리 쉐이킹 + @defer |
| TTFB | < 200ms | 정적 페이지용 CDN + SSG + 에지 캐싱 |
시리즈의 다음 기사에서는 Angular PWA(프로그레시브 웹 앱), 지원을 통해 Angular 애플리케이션을 설치 가능한 앱으로 전환하는 방법 분석 오프라인, 푸시 알림 및 서비스 워커를 통한 자동 업데이트.







