🚗

Angular v19のハイブリッドレンダリングを試す

2024/12/19に公開

本記事はAngularアドベントカレンダー2024の18日目の記事です。昨日はnao_yさんでした。
https://qiita.com/advent-calendar/2024/angular

はじめに

この記事では、Angular v19から開発者プレビューとして入ったサーバールーティングによるハイブリッドレンダリングを試して紹介します。

ハイブリッドレンダリングとは

ハイブリッドレンダリングは、サーバーサイドレンダリング(SSR)、プリレンダリングまたは静的サイト生成(SSG)、クライアントサイドレンダリング(CSR)を組み合わせて、フロントエンドアプリケーションを最適化する手法です。

各レンダリングの簡単な説明は、Angular公式ドキュメントにある次のようになっています。また公式ドキュメントでは、各レンダリングモードの利点や欠点も簡単にまとめられています。

レンダリングモード 説明
Server (SSR) 各リクエストに対してサーバーでアプリケーションをレンダリングし、完全に完成したHTMLページをブラウザに送信
Client (CSR) ブラウザでアプリケーションをレンダリング
Prerender (SSG) ビルド時にレンダリングし、各ルートの静的HTMLファイルを生成

(参考: サーバールーティングによるハイブリッドレンダリング・Angular 日本語版)

ここで扱うハイブリッドレンダリングに似た機能は、名前や概念は異なりますが他のメタフレームワークであるNext.jsNuxtAstroなどでもサポートされています。

実際に試す

準備

今回は、v19のAngularアプリケーションを新規作成してみます。

npx -p @angular/cli@19 ng new a19

SSR、SSGとServer Routing and App Engine APIsを有効にするか聞かれるので、Yesを選択します。

Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? Yes
Would you like to use the Server Routing and App Engine APIs (Developer Preview) for this server application? Yes

すると、ハイブリッドレンダリング用のアプリケーション構成が自動的に生成されます。
サーバー用のルーティング設定ファイルとして生成されたapp.routes.server.tsを見てみると、デフォルトでは全てのルートを事前レンダリングする設定になっていることがわかります。

app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: '**',
    renderMode: RenderMode.Prerender
  }
];

Angular v19が提供するサーバールーティングによるハイブリッドレンダリングは、その名の通りサーバールートごとにレンダリング方法を設定できるようになるものです。
ここでは、シンプルに3つのパターンを指定した場合の挙動を確認してみます。

app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'server',
    renderMode: RenderMode.Server, // サーバーサイドレンダリング(+ハイドレーション)
  },
  {
    path: 'client',
    renderMode: RenderMode.Client, // クライアントサイドレンダリング
  },
  {
    path: '**',
    renderMode: RenderMode.Prerender, // プリレンダリング(+ハイドレーション)
  },
];

挙動を確認するための簡単なアプリケーションを作成しました。
https://github.com/komura-c/a19-hybrid-rendering-sample/blob/main
APIサーバーから犬の画像を取得するためにDog APIを利用します。
このアプリケーションは、HttpClientを利用してHTTP通信を行いランダムな犬の画像のURLを取得したものを表示する1つのコンポーネントを、server, client, prerenderというパスのページコンポーネント内で同様に表示します。

app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: 'server', pathMatch: 'full' },
  {
    path: 'server',
    loadComponent: () =>
      import('./pages/server/server.component').then((c) => c.ServerComponent),
  },
  {
    path: 'client',
    loadComponent: () =>
      import('./pages/client/client.component').then((c) => c.ClientComponent),
  },
  {
    path: 'prerender',
    loadComponent: () =>
      import('./pages/prerender/prerender.component').then(
        (c) => c.PrerenderComponent
      ),
  },
  {
    path: '**',
    redirectTo: 'server',
  },
];

APIサーバーにリクエストしランダムな画像URLの取得により画像が変わるタイミングは、画像表示コンポーネントのngOnInit時と、ユーザーが再取得ボタンを押した時とします。

dogs-image-viewer.component.ts
  ngOnInit(): void {
    this.loadRandomDogImage();
  }
dogs-image-viewer.component.html
 <button (click)="loadRandomDogImage()">Load Random Dog Image</button>

デフォルトのpackage.jsonに記載の通り、次のコマンドでアプリケーションを実行します。

npm run build
npm run serve:ssr:a19 

RenderMode.Serverの挙動

RenderMode.Serverでは、次のような挙動になりました。

  • 初期ページロード時、SSRが実行されサーバーでコンポーネントがレンダリングされたHTMLが表示
  • ngOnInitはサーバーとブラウザ(クライアント)で実行される
  • ngOnInitHttpClientのAPIサーバーへのリクエストはサーバーでのみ実行される
    • TransferStateによってサーバーで取得したデータをクライアントに渡している
  • ハイドレーションが実行されたタイミングで、再取得ボタンへのイベントが接続されボタンが機能する
  • ボタンをクリックするたびにブラウザからAPIサーバーへリクエストが実行され、画像が変わる
  • ページをリロードすると、サーバーで毎回APIサーバーへリクエストが実行され、画像が変わる

注目した点

  • <script id="ng-state" type="application/json">というタグの中にAPIサーバーからのレスポンスと__nghData__という配列がありました。__nghData__をAngularのリポジトリで検索してみるとangular/angular/packages/core/src/hydration/utils.tsが見つかり、ハイドレーションで使われる情報が保存してあることが分かりました。

https://github.com/angular/angular/blob/19.0.4/packages/core/src/hydration/utils.ts#L43-L47

RenderMode.Clientの挙動

RenderMode.Clientでは、次のような挙動になりました。

  • 初期ページロード時、ビルド時にscriptタグを埋め込んだHTMLが表示、ブラウザ(クライアント)でコンポーネントがレンダリングされる
  • ngOnInitはブラウザでのみ実行される
  • ngOnInitHttpClientのAPIサーバーへのリクエストはブラウザでのみ実行される
  • ブラウザでSPAが起動したタイミングで、再取得ボタンへのイベントが接続されボタンが機能する
  • ボタンをクリックするたびにブラウザからAPIサーバーへリクエストが実行され、画像が変わる
  • ページをリロードすると、同様にブラウザから毎回APIサーバーへリクエストが実行され、画像が変わる

注目した点

  • 通常のSSRなしSPAのAngularアプリケーションと同じでした。

RenderMode.Prerenderの挙動

RenderMode.Prerenderでは、次のような挙動になりました。

  • 初期ページロード時、ビルド時にコンポーネントがレンダリングされたHTMLが表示
  • ngOnInitはビルド時とブラウザ(クライアント)で実行される
  • ngOnInitHttpClientのAPIサーバーへのリクエストはビルド時のみ実行される
  • ハイドレーションが実行されたタイミングで、再取得ボタンへのイベントが接続されボタンが機能する
  • ボタンをクリックするたびにブラウザからAPIサーバーへリクエストが実行され、画像が変わる
  • ページをリロードすると、ビルド時にAPIサーバーへリクエストが実行されたデータが変わらないため画像はずっと同じ

注目した点

  • RenderMode.Serverと同様に<script id="ng-state" type="application/json">というタグの中にAPIサーバーからのレスポンスと__nghData__という配列がありました。ビルド時にSSRしたHTMLを返し続ける以外はSSRと変わらない挙動に見えました。
  • リロードしていると時々、同じ画像が2回取得されることがありました(おそらくハイドレーション時にDOMが作り直されている)。開発者プレビューなので今後改善されるかもしれないですね。

おわりに

サーバールーティングによるハイブリッドレンダリングを試してみて、とても使いやすく便利だと感じました。

Angular Universalをv12ぐらいの時に試した時に、シンプルなアプリケーションではなかったからだと思いますが、dominoなどのライブラリを入れたり試行錯誤して挫折した経験があるため、ほとんど想定通りの挙動がデフォルトの設定の状態で簡単に確認でき良かったです。

今回はシンプルなアプリケーションで確認しただけなので、個人開発などでより複雑なアプリケーションにも導入してみたいです。

またv19のハイドレーション周りの新機能は、イベント再生(withEventReplay)や増分ハイドレーション(withIncrementalHydration)などもあるので、そちらも試してみたいと思いました!

ここまで記事をお読みいただきありがとうございました!

Angularアドベントカレンダー、明日はseapolisさんです。
https://qiita.com/advent-calendar/2024/angular

Voicyテックブログ

Discussion