Angular でコア Web Vitals を最適化する
I コアウェブバイタル Google が使用するパフォーマンス指標です。 Web サイトのユーザー エクスペリエンスの品質を決定するためのランキング要素。彼らのために Angular アプリケーションでは、これらのメトリクスの最適化は単なる技術的な問題ではありません。 検索エンジンでの可視性とユーザー満足度への直接投資。
このシリーズの 7 回目の記事では、 モダンアンギュラー 深く探っていきます
Core Web Vitals とは何か、その測定方法、そして何よりもアプリケーション内でそれらを最適化する方法
LCP、INP、CLS に特化したテクニックを備えた Angular。画像管理から
NgOptimizedImage とのバンドルの削減に @deferまで
Real User Monitoring による運用環境の監視。
この記事で学べること
- Core Web Vitals とは何ですか。また、それが Google のランキング要素となる理由
- NgOptimizedImage と事前接続を使用して Largest Contentful Paint (LCP) を最適化する方法
- シグナルによる Interaction to Next Paint (INP) を改善し、変更検出を減らすための戦略
- 明示的なディメンションとスケルトン画面で累積レイアウト シフト (CLS) を防ぐ方法
- Angular 固有の最適化: SSR、遅延読み込み
@defer、ルートベースのコード分割 - ツリーシェイキング、縮小化、ソースマップエクスプローラーによるバンドル分析と削減
- Web-Vital、Lighthouse、Chrome DevTools を使用したパフォーマンス測定
- Web 用の画像とフォントの高度な最適化
- Performance Observer API と GA4 を使用した実稼働環境のモニタリング
最新の Angular シリーズの概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | 角度信号 | きめ細かい応答性 |
| 2 | ゾーンレスの変更検出 | Zone.jsを削除する |
| 3 | 新しいテンプレート @if、@for、@defer | 最新の制御フロー |
| 4 | スタンドアロンコンポーネント | NgModule を使用しないアーキテクチャ |
| 5 | 信号形式 | シグナルを使用したレスポンシブフォーム |
| 6 | SSR と増分水分補給 | サーバーサイドレンダリング |
| 7 | 現在位置 → Angular におけるコア Web バイタル | パフォーマンスと指標 |
| 8 | 角度のある PWA | プログレッシブ Web アプリ |
| 9 | 高度な依存関係の注入 | DIツリーシェイク可能 |
| 10 | Angular 17 から 21 への移行 | 移行ガイド |
1. コア ウェブ バイタルとは
Core Web Vitals は、Google が定義した 3 つの側面を測定する一連の指標です ユーザーエクスペリエンスの基本: 読み込み速度, インタラクションに対する応答性 e 視覚的な安定性。 2021年から これらは正式に Google のランキング シグナルの一部です。つまり、サイトは Core Web Vitals が不十分な場合、検索結果でペナルティが課されます。
3 つの主要な Web Vitals 指標
| メトリック | 測定内容 | 良い | 改善されました | レア |
|---|---|---|---|---|
| LCP (最大のコンテンツを含むペイント) | 最大の表示要素をレンダリングする時間 | ≤ 2.5秒 | 2.5秒~4.0秒 | > 4.0秒 |
| INP (次のペイントへのインタラクション) | ユーザー操作とビジュアル更新の間の遅延 | ≤ 200ms | 200ミリ秒~500ミリ秒 | > 500ミリ秒 |
| CLS (累積レイアウトシフト) | ページ上の要素の予期しない移動 | ≤ 0.1 | 0.1~0.25 | > 0.25 |
FID から INP へ: 反応性指標の進化
2024 年 3 月に、Google は 最初の入力遅延 (FID) con 次のペイントへのインタラクション (INP) 公式の指標として。違い 基本的なのは、FID が測定したのは 遅れ 最初のインタラクションの間、 INP は次のレイテンシを測定します。 全て セッション全体を通してのやり取り、 最悪の値 (98 パーセンタイル) を返します。これにより、INP がメトリクスになります 実際のユーザーエクスペリエンスをよりよく表しています。
Angular にとってこれらが重要な理由
Angular アプリケーション、特にレンダリングを伴うシングル ページ アプリケーション (SPA) クライアント側では、歴史的に Core Web Vitals に問題があります。主な理由は次のとおりです。
- 重い JavaScript バンドル: Angular には、フレームワーク、コンポーネント、依存関係が 1 つの初期バンドルに含まれています
- 高価な変更の検出: 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 属性を忘れる
それなし 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 チェックリスト
- アメリカ合衆国
NgOptimizedImageconpriorityLCP画像上で - SSR が事前レンダリングされたコンテンツを含む HTML を送信できるようにする
- 追加
preconnectCDN 画像とフォントの場合 - アメリカ合衆国
preloadヒーロー画像とクリティカルフォント用 - 画像を WebP または AVIF に変換してサイズを削減します
- 自動スケーリング用の CDN イメージ ローダーを実装する
- 重要なリソースでのリダイレクトを回避します (リダイレクトごとに 300 ~ 500 ミリ秒が追加されます)
- 重要な CSS を縮小して HTML にインライン化する
3. 次のペイントへのインタラクションの最適化 (INP)
L'INP ユーザー操作 (クリック、 タップしてキーを押すと、画面上の次の視覚的な更新が行われます。 Angularの場合はこれ メトリクスは、変更検出、イベント ハンドラー、および使用状況によって直接影響されます。 Zone.js の。
変化の検出を減らすための信号
Angular での INP に対する最も影響力のある最適化の 1 つは、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 項目のリストのボタンをクリックします | 85ミリ秒 | 12ミリ秒 | -86% |
| 検索フィールドに入力する | 120ミリ秒 | 18ミリ秒 | -85% |
| 1000 行テーブルのフィルターを切り替えます | 250ミリ秒 | 45ミリ秒 | -82% |
| タブ間のナビゲーション | 65ミリ秒 | 8ミリ秒 | -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 予約済み |
| Cookie/通知バナー | 0.05~0.2 | オーバーレイ位置は固定されており、コンテンツはプッシュされません |
| サードパーティの広告と埋め込み | 0.1~0.4 | 予約されたディメンションを持つコンテナー |
5. 角度固有の最適化
Angular は、Core Web を大幅に改善するいくつかの組み込み機能を提供します
バイタル。 SSR、遅延ローディングとの組み合わせ @defer およびルートベースのコード
分割により、必要なときに必要なコードのみをロードするアーキテクチャが作成されます。
最適なLCPを実現するSSR
前の記事で見たように、サーバーサイド レンダリングは HTML を送信します。 ブラウザに事前にレンダリングされるため、ダウンロードと実行の待ち時間が不要になります。 JavaScriptの。これは、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秒 |
| ルート + @defer 分割 | 85KB | 0.9秒 | 1.3秒 |
| ルート+@defer+SSR | 85KB | 0.6秒 | 1.1秒 |
6. バンドルの最適化
JavaScript バンドルのサイズはロード時間に直接比例します。 そして解析。バンドルを減らすことは、すべてのメトリクスを改善することを意味します: LCP (JS の削減) ダウンロード)、INP (実行するコードが少ない)、および間接的に CLS (レンダリングが高速)。
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
ツリーシェイキングとビルドの最適化
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() - @angular/マテリアル: 実際に使用されるモジュール/コンポーネントのみをインポートします
7. パフォーマンスを測定する
測定しないものを最適化することはできません。コアを測定するためのツールがいくつかあります Web Vitals にはそれぞれ固有の利点があります。理想的な戦略は検査データを組み合わせる (Lighthouse、DevTools) と実際のユーザー データ (RUM、CrUX)。
ウェブバイタルライブラリ
図書館 web-vitals Google から提供されるのが最も簡単で信頼性の高い方法です。
ブラウザで Core Web Vitals を測定します。 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 のパフォーマンス | 実験室データ | すべて + フレームチャート | 長いタスクの詳細なデバッグ |
| ページスピードの洞察 | ラボ + フィールド | CWV + ヒント | CrUX データの概要 |
| ウェブバイタル (ライブラリ) | フィールドデータ(RUM) | LCP、INP、CLS、FCP、TTFB | 本番環境での実際のユーザーの監視 |
| Chrome UX レポート (CrUX) | フィールドデータ | CWV(実集計データ) | ランキングに使用される Chrome ユーザーの実データ |
| サーチコンソール | フィールドデータ | URLのCWV | SEOへの影響と問題のあるページ |
8. 高度な画像最適化
通常、画像は Web ページの総重量の 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% | 古いブラウザのフォールバック |
| WebP | -25-35% | 97%以上 | ほとんどの場合デフォルト |
| AVIF | -50% | 92%以上 | 写真、複雑な画像 |
| NPC | +200-400% | 100% | 必要な透明性のみを目的とする |
| SVG | 該当なし (ベクター) | 100% | シンプルなアイコン、ロゴ、グラフィックス |
9. フォントの最適化
Web フォントはレンダリングをブロックするリソースであることが多く、ブラウザーには フォントがダウンロードされるまでテキスト (FOIT - Flash of Invisible Text) または は、レイアウト シフトを引き起こす代替フォントを示しています (FOUT - フラッシュ スタイルのないテキスト)。どちらの動作も 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;
}
フォント表示戦略の比較
| 価値 | 行動 | 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 アナリティクス 4 との統合
GA4 は、カスタム イベントを通じて Core Web Vitals をネイティブにサポートします。組み合わせる
図書館 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),
});
}
}
}
パフォーマンス監視におけるよくある間違い
- ライトハウスだけを信頼してください: 臨床検査スコアは実際の状態を反映していません。 RUM は常に Lighthouse と一緒に使用してください
- 75 パーセンタイルを無視すると、次のようになります。 Google はランキングにフィールド データの 75 パーセンタイル (p75) を使用します。中央値では不十分です
- データをセグメント化しないでください。 デバイス (モバイル vs デスクトップ)、接続、地理位置情報ごとにメトリクスを分析
- ホームページのみを測定します。 各ページには異なる指標があります。多くの場合、最も多くのコンテンツ (ブログ、製品) を含むページが最も問題が発生します。
- sendBeacon は使用しないでください。 リクエスト
fetchユーザーがブラウズするときに削除できます。navigator.sendBeacon配達を保証します - 長いタスクを監視しないでください。 50 ミリ秒を超える長いタスクはメインスレッドをブロックし、INP を悪化させます。それらを監視してボトルネックを特定する
概要と次のステップ
Angular での Core Web Vitals の最適化は 1 回限りの操作ではなく、プロセスです 継続的な。メトリクスは、新しい機能、新しい依存関係、および新しい機能ごとに変化します。 フレームワークのアップデート。成功の鍵は組み合わせることです 測定値 続ける (RUMは生産中) プロアクティブな最適化 (SSR、 遅延読み込み、画像とフォントの最適化)。
この記事の重要な概念
- ウェブ上の重要な要素: LCP (≤ 2.5s)、INP (≤ 200ms)、および CLS (≤ 0.1) は Google のランキング要素です
- LCP: アメリカ合衆国
NgOptimizedImageconpriority、SSR、プリコネクトおよび最新のフォーマット (WebP/AVIF) - INP: シグナルに切り替えてグローバルな変更の検出を排除し、長いタスクを分割します。
scheduler.yield() - CLS: 画像/メディア上の明示的な寸法、スケルトン画面、
font-display: swapconsize-adjust - バンドル: ルートベースのコード分割 +
@defer初期 JS を 80% 以上削減 - 画像: デフォルトとして WebP、写真用に AVIF、
srcset応答性の高い自動遅延読み込み用 - フォント: セルフホスティング、サブセット、
font-display: optional最大限のパフォーマンスを実現するために - 測定: 本棚
web-vitals+ RUM 用の GA4、ラボデータ用の Lighthouse - 監視: パフォーマンス オブザーバー API、長時間タスクの検出、
sendBeacon確実な送信のために
Angular の推奨パフォーマンス目標
| メトリック | 客観的 | そこに到達する方法 |
|---|---|---|
| LCP | < 1.5秒 | SSR + NgOptimizedImage + プリロードヒーロー画像 |
| INP | < 100ms | ゾーンレス + シグナル + 最適化されたイベント ハンドラー |
| CLS | < 0.05 | 明示的な寸法 + スケルトン + フォント表示 (オプション) |
| 灯台のパフォーマンス | 90+ | すべての最適化を組み合わせたもの |
| 初期バンドル | < 100KB gzip圧縮 | コード分割 + ツリーシェイク + @defer |
| TTFB | < 200ms | 静的ページ用の CDN + SSG + エッジ キャッシュ |
シリーズの次の記事では、 Angular PWA (プログレッシブ ウェブ アプリ)、 Angular アプリケーションをサポート付きのインストール可能なアプリに変える方法を分析する オフライン、プッシュ通知、Service Worker による自動更新。







