Angular SSR と増分ハイドレーション: 2025 年のサーバーサイド レンダリング
Il サーバーサイド レンダリング (SSR) Angular では根本的な変革が行われました。 外部および実験的なパッケージとしての Angular Universal から、SSR が機能になりました ファーストクラスはフレームワークコアに統合されています。 Angular 19 以降では、 増分水分補給 ゲームのルールが変わりました: 水分補給はもう必要ありません アプリケーション全体を一度に実行できますが、ユーザーが実際に使用する部分のみが実行されます。
このシリーズの 6 回目の記事では、 モダンアンギュラー SSR アーキテクチャを調査します Angular の基本構成から高度な増分ハイドレーション戦略まで、 イベントのリプレイ、静的プリレンダリング、SEO の最適化。これらのテクノロジーがどのように機能するかを見ていきます。 具体的に私に影響を与える コアウェブバイタル アプリケーションを構成するようなものです 最大限のパフォーマンスを得るために角度を付けます。
この記事で学べること
- Angular でのサーバーサイド レンダリングの仕組みと Angular Universal からの進化
- 完全なアプリケーション ハイドレーション メカニズム: DOM の再利用とイベントの再生
- 完全な水分補給の限界とインタラクティブまでの時間 (TTI) への影響
- トリガーによる増分水分補給
@defer: ビューポート、インタラクション、アイドル、タイマー - 完全な SSR 構成:
app.config.server.ts、エクスプレスサーバー、プロバイダー - ハイドレーション中のインタラクションをキャプチャするためのイベント リプレイと jsaction との統合
- 静的ページ生成のためのプリレンダリング (SSG)
- メタタグ、構造化データ、クローラーレンダリングによるSEOの最適化
- 実践的なパターン:
isPlatformBrowser,TransferState、ブラウザ専用API - パフォーマンス戦略: 部分的な水分補給、バンドルの最適化、Core Web Vitals
最新の 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. Angular でのサーバー側レンダリング
サーバーサイド レンダリングは、ページの HTML を生成することで構成されます。 サーバー上で ブラウザの代わりに。ユーザーまたはクローラーがページをリクエストすると、サーバーが実行します。 Angular アプリケーションは完全な HTML を生成し、クライアントに送信します。ブラウザに表示されるのは、 JavaScript がダウンロードされて実行されるのを待つことなく、コンテンツをすぐに閲覧できます。
進化: Angular Universal から Core へ
歴史的に、Angular の SSR は次によって管理されていました。 角度ユニバーサル、パッケージ
別れた(@nguniversal/express-engine) 手動構成が必要でした
重要な。 Angular 17 以降では、SSR がフレームワークに直接統合されています。
@angular/ssr Angular Universal を置き換え、CLI が自動的に生成されます。
必要な設定ファイル。
Angular における SSR の進化
| バージョン | SSR技術 | 特徴 |
|---|---|---|
| 角2-16 | Angular Universal (@nguniversal) | 外部パッケージ、手動セットアップ、水分補給なし |
| 角度 16 | Angular Universal + Hydration (開発者プレビュー) | 初の非破壊的なハイドレーション、DOM の再利用 |
| 角度 17 | @angular/ssr (新しいパッケージ) | CLI に組み込まれた SSR、安定したハイドレーション、@defer サポート |
| 角度 19 | @angular/ssr + 増分ハイドレーション | 増分ハイドレーション、イベントのリプレイ、ルートレベルのレンダリング |
| 角度 21 | @angular/ssr (安定版) | 安定した増分水分補給、改善された SSG、最適化されたパフォーマンス |
SSR フローの仕組み
Angular における SSR リクエストのライフサイクルは、次のような正確なフローに従います。 サーバー、ブラウザ、ハイドレーションプロセス。
- HTTPリクエスト: ブラウザ (またはクローラー) が URL をリクエストします。
- サーバー側のレンダリング: Express がリクエストを受信し、Angular がアプリを実行して HTML を生成します。
- HTML の送信: サーバーは、レンダリングされたコンテンツを含む完全な HTML を送信します。
- 最初のコンテンツフルペイント: ブラウザにコンテンツがすぐに表示されます
- JSのダウンロード: ブラウザはアプリケーションの JavaScript バンドルをダウンロードします
- 水分補給: Angular は静的 HTML を「復活」させ、イベント リスナーと応答性を接続します
- インタラクティブなアプリケーション: アプリが完全に機能するようになります
# Nuovo progetto con SSR abilitato
ng new my-ssr-app --ssr
# Aggiungere SSR a un progetto esistente
ng add @angular/ssr
# Il comando genera automaticamente:
# - server.ts (Express server)
# - src/app/app.config.server.ts (configurazione server)
# - Aggiorna angular.json con target "server"
2. 完全な塗布による水分補給
La 水分補給 Angular が静的 HTML を変換するプロセスです ブラウザの対話型アプリケーションでサーバーによって生成されます。 Angular 16 より前では、 単純にフレームワーク 破壊された サーバーの HTML からすべてを再レンダリングしました ブラウザではゼロ (破壊的なハイドレーション)。これにより、コンテンツの迷惑な「フラッシュ」が発生しました SSR の利点の多くが無効化されました。
非破壊的な水分補給
Angular 16 以降、ハイドレーションは次のようになりました。 非破壊的: 角度のある サーバー生成の DOM ノードを再作成するのではなく、再利用します。これにより視覚的なフラッシュが排除されます 大幅に改善されます 最大のコンテンツフル ペイント (LCP) なぜなら、 ブラウザはレイアウトをやり直す必要はありません。
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
// Abilita la hydration non-destructive
provideClientHydration(),
]
};
DOM 再利用の仕組み
サーバー側のレンダリング中に、Angular は構造情報を HTML にシリアル化します。
などの特別な属性を通じて ngSkipHydration そして隠しコメント。いつ
クライアントは HTML を受信し、フレームワークであるハイドレーションを開始します。
- 新しい DOM を作成するのではなく、既存の DOM を走査します。
- イベント リスナーを既存の DOM ノードにアタッチする
- コンポーネントの内部状態を初期化します。
- サーバーの DOM がクライアントの出力と一致することを確認します。
水分補給の不一致: よくある間違い
サーバーによって生成された HTML がクライアントが期待するものと一致しない場合、Angular エラーが発生します 水分補給の不一致。最も一般的な原因は次のとおりです。
- へのアクセス
window,documentolocalStorageサーバーレンダリング中 - の使用
Date.now()oMath.random()サーバーとクライアントで異なる値を生成します - による直接 DOM 操作
ElementRef.nativeElement - ブラウザのみの機能 (ビューポート サイズなど) に基づく条件付きコンテンツ
- DOM を直接変更するサードパーティ ライブラリ
3. 十分な水分補給の問題
Con la フルアプリケーション水分補給、Angularは水分補給が必要です 全体 アプリケーションがインタラクティブになる前に。つまり、 ユーザーは決して見ることはできません(ページ下部のフッターや非表示のセクションがどのように表示されるか) 積載時に水分を補給し、 インタラクティブ化までの時間 (TTI).
完全な水分補給がコア ウェブ バイタルに及ぼす影響
| メトリック | SSR(CSR)なし | SSR+フルハイドレーション | 問題 |
|---|---|---|---|
| 最初のコンテンツフル ペイント (FCP) | 2.5秒 | 0.8秒 | 大幅に改善されました |
| 最大のコンテンツフル ペイント (LCP) | 3.2秒 | 1.2秒 | 大幅に改善されました |
| インタラクティブ化までの時間 (TTI) | 3.5秒 | 3.0秒 | 改善はほとんどありません: アプリ全体を保湿する必要があります |
| 合計ブロッキング時間 (TBT) | 800ミリ秒 | 650ミリ秒 | まだ高い:水分補給は長い作業です |
| 次のペイントへのインタラクション (INP) | 120ミリ秒 | 180ミリ秒 | 悪化: 水分補給中にイベントを見逃す |
フル ハイドレーションのパラドックスは、初期レンダリング メトリクス (FCP、LCP) が向上することです。 しかし、それは対話性の指標を解決するものではなく、場合によっては悪化させることもあります。理由は簡単です。 数百のコンポーネントのハイドレーションは、メインスレッドをブロックする CPU 集中型の操作です。
完全な水分補給は高価なので
- すべての JavaScript をダウンロードします。 決して表示されないコンポーネントであっても、ブラウザはコードをダウンロードして解析する必要があります。
- 各コンポーネントの実行: Angular では、各コンポーネントを初期化し、依存関係注入ツリーを作成し、バインディングを接続する必要があります。
- メインスレッドのブロック: ハイドレーションは同期的に発生し、ユーザーの操作をブロックします
- モバイルでのリソースの無駄: CPU と帯域幅が限られているデバイスが最も影響を受けます
4. 段階的な水分補給
La 増分水分補給 完全な水分補給の問題を解決します。
成分に水分を与えるため オンデマンド、必要な場合のみ。開発者として紹介されました
Angular 19 のプレビューは Angular 21 で安定しました。この機能はブロックに基づいています。
@defer Angular 構文テンプレートにすでに存在します。
仕組み
このアイデアはシンプルですが強力です。コンポーネントをブロックでラップするというものです。 @defer 彼らは来ます
サーバー上では通常どおりレンダリングされますが (ユーザーにはコンテンツが表示されます)、クライアントでは
水分補給が来る 延期した 特定のトリガーが来るまで
活性化された。その間、サーバーの静的 DOM は表示されたままですが、対話型ではありません。
<!-- Questa sezione viene renderizzata sul server ma idratata
solo quando entra nel viewport dell'utente -->
@defer (hydrate on viewport) {
<app-product-list [category]="selectedCategory" />
}
<!-- Questo componente viene idratato quando l'utente
interagisce con esso (click, focus, hover) -->
@defer (hydrate on interaction) {
<app-comment-section [postId]="post.id" />
}
<!-- Questo viene idratato quando il browser e idle,
cioe non sta eseguendo altri task -->
@defer (hydrate on idle) {
<app-sidebar-recommendations />
}
<!-- Questo viene idratato dopo un timer specifico -->
@defer (hydrate on timer(3s)) {
<app-analytics-widget />
}
<!-- Mai idratato: resta HTML statico per sempre -->
@defer (hydrate never) {
<app-static-footer />
}
<!-- Idratato immediatamente (equivale a full hydration) -->
@defer (hydrate on immediate) {
<app-critical-cta />
}
水分補給トリガーが利用可能
| トリガー | 構文 | 起動時 | 使用事例 |
|---|---|---|---|
| ビューポート | hydrate on viewport |
コンポーネントが表示ビューポートに入ります | スクロールせずに見えるコンテンツ、製品リスト |
| 交流 | hydrate on interaction |
要素をクリック、フォーカス、タッチします | フォーム、ボタン、インタラクティブ領域 |
| アイドル状態 | hydrate on idle |
ブラウザにはキューにタスクがありません | 二次ウィジェット、サイドバー |
| タイマー | hydrate on timer(Ns) |
N 秒間の読み込み後 | 分析、追跡、遅延ウィジェット |
| すぐに | hydrate on immediate |
ロード直後 | 重要なコンポーネントをスクロールせずに見える範囲に配置 |
| 一度もない | hydrate never |
しない: 静的な HTML のままにする | フッター、純粋に有益なコンテンツ |
増分水分補給を有効にする
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import {
provideClientHydration,
withIncrementalHydration
} from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Abilita hydration con supporto incrementale
provideClientHydration(
withIncrementalHydration()
),
]
};
増分水分補給の要件
- 参照されるコンポーネントは次のとおりである必要があります スタンドアロン (NgModuleには依存できません)
- 機能
withIncrementalHydration()も必要ですprovideClientHydration() - 中身は
@deferまだサーバー上でレンダリングされます - トリガーはチェックするだけです いつ クライアントに水分補給されています - トリガー
@placeholdere@loadingdi@deferこれらは SSR では使用されません。サーバーは完全なコンテンツを直接表示します。
5. SSR 構成を完了する
Angular での本番環境に対応した SSR セットアップには、いくつかのファイルが必要です。見てみましょう 各コンポーネントの完全な構造と役割。
サーバー側の構成
// src/app/app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRoutesConfig } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
// Abilita il rendering lato server
provideServerRendering(),
// Configurazione route-level rendering
provideServerRoutesConfig(serverRoutes),
]
};
// Merge della config client con quella server
export const config = mergeApplicationConfig(appConfig, serverConfig);
ルートレベルのレンダリング設定
// src/app/app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
renderMode: RenderMode.Prerender, // SSG: generato al build time
},
{
path: 'blog',
renderMode: RenderMode.Prerender, // Pagina statica
},
{
path: 'blog/:slug',
renderMode: RenderMode.Server, // SSR: generato ad ogni richiesta
},
{
path: 'dashboard',
renderMode: RenderMode.Client, // CSR: solo lato client
},
{
path: '**',
renderMode: RenderMode.Server, // Default: SSR per tutto il resto
},
];
エクスプレスサーバー
// server.ts
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './src/main.server';
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
// Serve file statici dalla cartella browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y', // Cache aggressiva per asset statici
index: false,
}));
// Tutte le altre route passano per Angular SSR
server.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl }
],
})
.then(html => res.send(html))
.catch(err => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
const server = app();
server.listen(port, () => {
console.log(`Server SSR in ascolto su http://localhost:${port}`);
});
}
run();
SSR ファイル構造
| ファイル | 役割 |
|---|---|
src/app/app.config.ts |
クライアント構成: ルーター、ハイドレーション、プロバイダー |
src/app/app.config.server.ts |
サーバー構成: ProvideServerRendering、serverRoutes |
src/app/app.routes.server.ts |
各ルート (Prerender、Server、Client) の renderMode を定義します。 |
server.ts |
SSRリクエストを処理するExpressサーバー |
src/main.server.ts |
サーバー側ブートストラップのエントリ ポイント |
6. イベントリプレイ
従来の SSR の最もイライラする問題の 1 つは、いわゆる 「不気味の谷」: ユーザーはレンダリングされたページを見て操作を開始します (ボタンをクリックし、フォームに記入します)。 しかし、ハイドレーションがまだ完了していないため、アプリケーションは応答しません。イベントが来る 失った そしてユーザーはそのアクションを繰り返す必要があります。
イベントリプレイの仕組み
Angular はこの問題を次の方法で解決します。イベントリプレイ、ライブラリに基づいて
jsaction Googleによる。このメカニズムは 3 つの段階で機能します。
- 捕獲: 小さなインライン スクリプト (サーバーから HTML に挿入される) が、Angular の準備が整う前にすべてのユーザー イベントをキャッチします。
- バッファ: イベントは、必要なすべての情報 (イベント タイプ、ターゲット、タイムスタンプ) とともにキューに保存されます。
- リプレイ: コンポーネントのハイドレーションが完了すると、イベントが正しい順序で再ディスパッチされます。
// app.config.ts
import {
provideClientHydration,
withEventReplay,
withIncrementalHydration
} from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(
// Event Replay: cattura gli eventi durante la hydration
withEventReplay(),
// Incremental Hydration: hydrate on demand
withIncrementalHydration(),
),
]
};
イベントリプレイ: 増分水分補給による動作
インクリメンタル ハイドレーションでは、イベント リプレイがさらに重要になります。とき
ユーザーが、ハイドレーションが延期されるコンポーネントをクリックします (例: hydrate on interaction)、
次のようなことが起こります。
- イベントはjsactionによってキャプチャされます
- クリックトリガーによりコンポーネントの水和が活性化されます。
- Angular はコンポーネントの遅延コードをダウンロードしてハイドレートします
- キャプチャされたイベントは、対話型になったコンポーネントに再ディスパッチされます。
- ユーザーは重大な遅延を感じません
7. プリレンダリング(SSG)
Il プリレンダリング (または静的サイト生成、SSG) ページの HTML を生成します。 アル ビルド時間 すべてのリクエストの代わりに。ページは HTML ファイルとして保存されます 静的であり、サーバーを必要とせずに CDN または Web サーバーから直接提供されます。 Node.js が実行されています。
SSG と SSR を使用する場合
SSG 対 SSR: 選択ガイド
| 基準 | SSG (プリレンダー) | SSR(サーバー) |
|---|---|---|
| コンテンツ | 静的でめったに変更されない | ユーザーごとに動的にパーソナライズ |
| パフォーマンス | 非常に高速 (ファイルは CDN によって提供される) | サーバーのレンダリング時間に依存します |
| インフラストラクチャー | 静的ホスティングのみ (Firebase、Netlify、S3) | Node.jsサーバーが実行中 |
| スケーラビリティ | 優れています (CDN が負荷を処理します) | サーバーのスケーリングが必要 |
| アップデート | 再構築と展開が必要 | コンテンツは常に更新されます |
| 典型的な使用例 | ブログ、ランディング ページ、ドキュメント、ポートフォリオ | Eコマース、ダッシュボード、ソーシャルメディア |
プリレンダリングの構成
// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
// Pagine statiche: generate al build time
{
path: '',
renderMode: RenderMode.Prerender,
},
{
path: 'about',
renderMode: RenderMode.Prerender,
},
// Route parametriche: elenco esplicito di path
{
path: 'blog/:slug',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
// Restituisce un array di oggetti con i parametri
return [
{ slug: 'angular-ssr-hydration' },
{ slug: 'angular-signals-guida' },
{ slug: 'zoneless-change-detection' },
];
},
},
// Route dinamiche: SSR a runtime
{
path: 'product/:id',
renderMode: RenderMode.Server,
},
// Dashboard: solo client-side
{
path: 'admin/**',
renderMode: RenderMode.Client,
},
];
RenderMode: 3 つの戦略
- RenderMode.Prerender (SSG): ビルド時に生成される HTML。静的コンテンツに最適です。非常に高速で、実行時にサーバーは必要ありません
- レンダーモード.サーバー (SSR): リクエストごとにサーバー上で生成される HTML。パーソナライズされたコンテンツまたは頻繁に変更されるコンテンツに必要
- レンダーモード.クライアント (CSR): サーバーレンダリングはなく、HTML は完全にブラウザーによって生成されます。安全なエリアまたはインタラクティブなダッシュボードに役立ちます
8. SSRによるSEO
SSR は検索エンジンのクローラーが読み取ることができるため、SEO にとって非常に重要です。 JavaScript を実行することなく、サーバーによって生成された HTML を直接取得できます。しかし、レンダリングは それだけでは不十分です。構造化されたメタタグが必要です。JSON-LD データは適切に管理されます。 正規 URL の数。
タイトルとメタサービスを含む動的メタタグ
// seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
import { DOCUMENT } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class SeoService {
private title = inject(Title);
private meta = inject(Meta);
private document = inject(DOCUMENT);
updatePageMeta(config: {
title: string;
description: string;
url: string;
image?: string;
type?: string;
}): void {
// Title tag
this.title.setTitle(config.title);
// Meta description
this.meta.updateTag({
name: 'description',
content: config.description
});
// Open Graph tags
this.meta.updateTag({ property: 'og:title', content: config.title });
this.meta.updateTag({ property: 'og:description', content: config.description });
this.meta.updateTag({ property: 'og:url', content: config.url });
this.meta.updateTag({ property: 'og:type', content: config.type || 'website' });
if (config.image) {
this.meta.updateTag({ property: 'og:image', content: config.image });
}
// Twitter Card
this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
this.meta.updateTag({ name: 'twitter:title', content: config.title });
this.meta.updateTag({ name: 'twitter:description', content: config.description });
// Canonical URL
this.setCanonicalUrl(config.url);
}
addStructuredData(data: object): void {
const script = this.document.createElement('script');
script.type = 'application/ld+json';
script.textContent = JSON.stringify(data);
this.document.head.appendChild(script);
}
private setCanonicalUrl(url: string): void {
let link: HTMLLinkElement | null =
this.document.querySelector('link[rel="canonical"]');
if (!link) {
link = this.document.createElement('link');
link.setAttribute('rel', 'canonical');
this.document.head.appendChild(link);
}
link.setAttribute('href', url);
}
}
構造化データ (JSON-LD)
// In un componente articolo blog
@Component({ /* ... */ })
export class ArticlePageComponent {
private seo = inject(SeoService);
ngOnInit(): void {
const article = this.articleData();
this.seo.updatePageMeta({
title: `${article.title} | Federico Calo`,
description: article.summary,
url: `https://federicocalo.dev/blog/${article.slug}`,
image: 'https://federicocalo.dev/assets/og-image.jpg',
type: 'article',
});
// Structured Data per Google
this.seo.addStructuredData({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.summary,
author: {
'@type': 'Person',
name: 'Federico Calo',
url: 'https://federicocalo.dev',
},
datePublished: article.date,
image: 'https://federicocalo.dev/assets/og-image.jpg',
publisher: {
'@type': 'Organization',
name: 'Federico Calo Dev',
url: 'https://federicocalo.dev',
},
});
}
}
Angular SSR アプリケーションの SEO チェックリスト
- ページごとに一意のタイトルタグ: 最大60文字、主要キーワードを含む
- メタディスクリプション: 150~160 文字、ページの魅力的な要約
- グラフと Twitter カードを開く: ソーシャルメディアで適切にプレビューするには
- 正規の URL: 重複コンテンツの問題を防ぐ
- JSON-LD 構造化データ: 記事、人物、組織、ブレッドクラムリスト
- サイトマップ.xml: 多言語サイト向けに hreflang を使用して自動生成
- ロボット.txt: 重要なページのクロールを許可するように構成されています
- 見出し階層: H1 はページごとに 1 つだけ、H2 ~ H6 は論理的な順序で
9. SSRの実践パターン
SSR を使用するには、サーバー環境とブラウザ環境の違いに注意する必要があります。
多くのブラウザ API ( window, localStorage, document.querySelector)
サーバー側のレンダリングでは使用できません。管理すべき重要なパターンは次のとおりです
これらの状況。
isPlatformBrowser と isPlatformServer
import { Component, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({
selector: 'app-analytics',
standalone: true,
template: `<div #trackingContainer></div>`
})
export class AnalyticsComponent {
private platformId = inject(PLATFORM_ID);
ngOnInit(): void {
if (isPlatformBrowser(this.platformId)) {
// Sicuro: eseguito solo nel browser
this.initGoogleAnalytics();
this.trackPageView();
}
if (isPlatformServer(this.platformId)) {
// Eseguito solo sul server
console.log('Rendering lato server per:', this.route);
}
}
private initGoogleAnalytics(): void {
// Accesso sicuro a window e document
const gtag = (window as any).gtag;
if (gtag) {
gtag('config', 'G-XXXXXXXXXX');
}
}
private trackPageView(): void {
// localStorage disponibile solo nel browser
const views = localStorage.getItem('pageViews') || '0';
localStorage.setItem('pageViews', String(Number(views) + 1));
}
}
TransferState: 重複した HTTP リクエストを回避する
サーバーがページをレンダリングする API リクエストを行うとき、クライアントは 同じリクエストを繰り返す必要があります。の 転送状態 できるようにします HTML にデータを埋め込んでサーバーからクライアントにデータを転送します。
import {
Injectable, inject, PLATFORM_ID,
TransferState, makeStateKey
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
interface Product {
id: number;
name: string;
price: number;
}
const PRODUCTS_KEY = makeStateKey<Product[]>('products');
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
getProducts(): Observable<Product[]> {
// 1. Controlla se i dati sono già nel TransferState
const cached = this.transferState.get(PRODUCTS_KEY, null);
if (cached) {
// Dati trasferiti dal server: usali e rimuovili
this.transferState.remove(PRODUCTS_KEY);
return of(cached);
}
// 2. Nessun dato in cache: fai la richiesta HTTP
return this.http.get<Product[]>('/api/products').pipe(
tap(products => {
if (isPlatformServer(this.platformId)) {
// 3. Sul server: salva nel TransferState
this.transferState.set(PRODUCTS_KEY, products);
}
})
);
}
}
HttpClient と自動 TransferState
Angular 18 以降、 provideClientHydration() 自動的に含まれる
HTTP リクエストのキャッシュ withHttpTransferCacheOptions()。
これは、ほとんどの場合、手動で管理する必要がないことを意味します。
HTTP 呼び出しの TransferState: Angular がそれを行います。
afterNextRender と afterRender
レンダリング後にブラウザーでのみ実行する必要があるコードの場合、Angular は次の機能を提供します。 SSR セーフでゾーンレス互換の 2 つの専用 API。
import { Component, afterNextRender, afterRender, ElementRef, viewChild } from '@angular/core';
@Component({
selector: 'app-chart',
standalone: true,
template: `<canvas #chartCanvas width="800" height="400"></canvas>`
})
export class ChartComponent {
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
constructor() {
// Eseguito UNA VOLTA dopo il primo render nel browser
// NON eseguito sul server
afterNextRender(() => {
const ctx = this.canvas().nativeElement.getContext('2d');
this.initChart(ctx!);
});
// Eseguito OGNI VOLTA che Angular rende il componente
// Utile per sincronizzare con librerie DOM esterne
afterRender(() => {
this.updateChartDimensions();
});
}
private initChart(ctx: CanvasRenderingContext2D): void {
// Inizializza un chart con Canvas API
// Sicuro: eseguito solo nel browser
}
private updateChartDimensions(): void {
// Aggiorna dimensioni basate sul viewport
}
}
10. パフォーマンスの最適化
SSR と増分水分補給は、Core Web Vitals に直接影響します。見てみましょう パフォーマンスを最大化するための具体的な戦略。
コンテンツタイプ別の水分補給戦略
水分補給戦略マップ
| ページエリア | 戦略 | モチベーション |
|---|---|---|
| ナビゲーションバーとヒーローセクション | hydrate on immediate |
スクロールせずに見える範囲での即時対話が必要 |
| 主なコンテンツ(記事、商品) | 完全な水分補給 (@defer なし) | コアコンテンツ、LCP要素の可能性あり |
| コメントセクション | hydrate on viewport |
スクロールしないと見えない範囲に表示されます。 |
| お問い合わせフォーム | hydrate on interaction |
インタラクティブだが最初のロードでは重要ではない |
| サイドバーの推奨事項 | hydrate on idle |
第二に、ブラウザが空いているときに水分補給するため |
| 分析/追跡ウィジェット | hydrate on timer(5s) |
見えないので待てます |
| フッターと法的リンク | hydrate never |
純粋に有益であり、インタラクティブ性は必要ありません |
コアウェブバイタルへの影響
ベンチマーク: 50 以上の記事を含むブログ アプリケーション
| メトリック | CSRのみ | SSR+フルハイドレーション | SSR + 増分水分補給 |
|---|---|---|---|
| FCP | 2.4秒 | 0.8秒 | 0.7秒 |
| LCP | 3.1秒 | 1.1秒 | 0.9秒 |
| TTI | 3.8秒 | 3.2秒 | 1.5秒 |
| 未定 | 850ミリ秒 | 620ミリ秒 | 180ミリ秒 |
| INP | 110ミリ秒 | 170ミリ秒 | 65ミリ秒 |
| CLS | 0.15 | 0.02 | 0.01 |
| 初期JS | 320KB | 320KB | 85KB |
増分ハイドレーションによるバンドルの最適化
<!-- Layout pagina prodotto ottimizzato per Core Web Vitals -->
<!-- CRITICO: hydration immediata (above-the-fold) -->
<app-navbar />
<app-product-hero [product]="product()" />
<!-- VISIBILITA: hydration su viewport -->
@defer (hydrate on viewport) {
<app-product-details [product]="product()" />
}
@defer (hydrate on viewport) {
<app-product-reviews [productId]="product().id" />
}
<!-- INTERAZIONE: hydration su input utente -->
@defer (hydrate on interaction) {
<app-add-to-cart [product]="product()" />
}
<!-- BACKGROUND: hydration quando il browser e libero -->
@defer (hydrate on idle) {
<app-related-products [categoryId]="product().categoryId" />
}
<!-- DIFFERITO: hydration dopo timer -->
@defer (hydrate on timer(5s)) {
<app-user-tracking [productId]="product().id" />
}
<!-- STATICO: mai idratato -->
@defer (hydrate never) {
<app-footer />
}
影響の測定
// performance-monitor.service.ts
import { Injectable, inject, PLATFORM_ID, afterNextRender } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class PerformanceMonitorService {
private platformId = inject(PLATFORM_ID);
constructor() {
afterNextRender(() => {
this.observeWebVitals();
});
}
private observeWebVitals(): void {
if (!isPlatformBrowser(this.platformId)) return;
// Largest Contentful Paint
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime, 'ms');
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// First Input Delay
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries() as PerformanceEventTiming[];
entries.forEach(entry => {
console.log('FID:', entry.processingStart - entry.startTime, 'ms');
});
});
fidObserver.observe({ type: 'first-input', buffered: true });
// Cumulative Layout Shift
const clsObserver = new PerformanceObserver((list) => {
let clsScore = 0;
list.getEntries().forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
});
console.log('CLS:', clsScore);
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
}
}
SSR で避けるべきよくある間違い
- へのアクセス
window見えないところ: 常に使用するisPlatformBrowseroafterNextRender - 水分補給の不一致: サーバーとクライアント間で異なるコンテンツを生成しないでください (例:
Date.now()、ランダムデータ) - ブラウザ専用ライブラリのインポート: Chart.js や Leaflet などのライブラリは、ブラウザの動的インポートを使用してインポートする必要があります
- TransferState が使用されていない場合: TransferState (または Angular 18 以降の自動キャッシュ) がないと、HTTP 呼び出しが 2 回行われます
- あらゆるものの水分補給: 増分ハイドレーションが利用できるため、アプリ全体をハイドレーションするのは無駄です。アメリカ合衆国
hydrate never静的コンテンツの場合 - CLS を無視します。 Web フォント、無次元画像、動的に読み込まれるコンテンツによりレイアウトが変化する
概要と次のステップ
Angular の SSR は、パッケージとしての Angular Universal から、質的に大きな飛躍を遂げました。 統合された高性能かつ柔軟なソリューションの外部にあります。増分水分補給 本当の革命を表し、最良の初期レンダリングを組み合わせることができます 即時的かつターゲットを絞った双方向性により可能になります。
この記事の重要な概念
- Angular の SSR: コアに統合されている
@angular/ssr、Angular Universal を置き換えます - 完全な水分補給: サーバー DOM を再利用しますが、すべてを一度にハイドレートするため、TTI に影響します
- 段階的な水分補給: トリガーを使用してオンデマンドでコンポーネントをハイドレートする
@defer(ビューポート、インタラクション、アイドル、タイマー、なし) - イベントリプレイ: 水分補給中のユーザー インタラクションと、水分補給後の再ディスパッチをキャプチャします。
- プリレンダリング (SSG): CDN によって提供される静的ページのビルド時に HTML を生成します
- レンダリングモード: プリレンダー、サーバー、クライアントの間でルートごとに選択
- SEO: 可視性を最大化するメタタグ、オープングラフ、JSON-LD構造化データ
- SSRパターン:
isPlatformBrowser,TransferState,afterNextRender - パフォーマンス: 増分水分補給により、TTI が 50% 以上、TBT が 70% 以上削減されます
シリーズの次の記事では、 Angular におけるコア Web バイタル、 FCP、LCP、CLS、INP、TTIを測定、監視、最適化する方法を詳しく調べる すべてのカテゴリで Lighthouse スコア 90 以上を達成すること。







