💧

AngularのIncremental Hydrationの挙動を確認する

に公開

本記事はAngularアドベントカレンダー2025の14日目の記事です。
昨日は@ic_lifewoodさんでした。

はじめに

Angular v17以降、Angularでは@angular/ssrパッケージの開発によりServer Side Rendering(SSR)やPrerenderingの機能が充実してきました。この記事では、Angular v20でstableになった機能の1つであるIncremental Hydration(インクリメンタルハイドレーション)の概要と実際の挙動を紹介します。

Hydrationとは

Incremental Hydrationの前に、Hydrationとは何かに触れておきます。
Hydrationとは、SSRやPrerenderingにおいてサーバー側がレンダリングしたアプリケーションをクライアント側で復元するプロセスのことです。
(参考: ハイドレーション • Angular 日本語版)

サーバー側でレンダリングされた静的なHTMLには、ボタンのクリックなどのイベント処理を行うJavaScriptのロジックが組み込まれていません。そこでクライアント側でJavaScriptを読み込み、Hydrationのプロセスで既存のDOM要素にイベントリスナーを正しく接続します。
これにより、ユーザーがボタンをクリックしたり、フォームに入力したりといった操作が可能になります。他にも、サーバー側でレンダリングの際に構築した状態や取得したデータの転送などもHydrationの中で行います。

ここで、問題が発生します。
クライアント側はサーバー側でレンダリングされた静的なHTMLを、Hydrationを待たずにすぐにユーザーに表示します。そのためHydrationが完了するまではユーザーがボタンをクリックするなどのアクションを起こしても、何も反応しないという時間が発生します。
これを不気味の谷(Uncanny Valley)と呼んでいる人たちもいます。(初出が分かりませんが、Solidを開発したRyan CarniatoがResumability, WTF? - DEV Communityで"Enter the Uncanny Valley"と書いていました)

これを軽減するための方法として、AngularではEvent Replayという機能が開発されました。
これはハイドレーションが完了する前に発生したすべてのイベントを事前にキャプチャしておき、ハイドレーションが完了した後にそれらのイベントをリプレイできるようにする機能です。
これが有効であり、Hydrationの完了時間が短ければ短いほど、ユーザーイベントへの反応が不快なほど遅れるということはなくなります。

そこで本当に必要なHydrationを優先し完了するまでの時間を短縮、Hydrationを効率的に行うための機能がIncremental Hydrationになります。

Incremental Hydrationとは

Incremental Hydrationとは、クライアントのレンダリング時に一部をHydrationしない状態にし、設定したトリガーによって段階的にHydrationを行うことができる機能です。
(参考: インクリメンタルハイドレーション • Angular 日本語版)

現在は、Angular v17で登場したDeferred Views@deferを利用することでどこまでHydrationを遅延するかのトリガーを設定できます。

Incremental Hydrationを有効にするには、公式ドキュメントの通り次の記述を入れます。
Incremental Hydrationはイベントリプレイに依存している機能のため、イベントリプレイが自動的に有効になるので、withEventReplayの記述はなくても大丈夫です。

app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
-    provideClientHydration(withEventReplay())
+    provideClientHydration(withEventReplay(), withIncrementalHydration())
  ]
};

挙動を確認する

今回は、ng newでAngularプロジェクトを構築した後に、Geminiと共にデモアプリケーションを作成しました。(Antigravity上でAngular CLI MCPを接続したGemini3 Proを利用しました)

次のような条件で確認します。

  • v21のAngularでデフォルトであるServer.Prerender、つまりPrerenderingを選択している時のHydration
  • Chrome DevToolsでネットワークを低速 4Gにエミュレート(挙動が分かりやすいため)

対象は現在公式ドキュメントにある次のトリガーです。

  • hydrate on
    • hydrate on immediate
    • hydrate on idle
    • hydrate on timer
    • hydrate on interaction
    • hydrate on hover
    • hydrate on viewport
  • hydrate when
  • hydrate never

HydrationがまだのセクションはServer Rendered, Hydration済みのものはHydratedというバッジを表示しています。これは、isPlatformBrowserを利用して切り替えています。
今回は検証のために入れていますが、ユーザートリガーでないHydrationでサーバーとクライアントのレンダリング時で異なるコンテンツを表示するのは、ユーザー体験やCoreWebVitalsのCLS(Cumulative Layout Shift)的に良くないため避けるべきです。
公式ドキュメントにも記述があります。

protected isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

ビルド時の確認

ng newの際にSSRを選択する、または既存プロジェクトでng add @angular/ssrしてSSRを導入すると、ビルド用のnpm scriptsがpackage.jsonに作成されます。それに沿い、次のようなコマンドで確認することができます。

npm run build
npm run serve:ssr:${プロジェクト名}

ビルドの生成物を確認すると、@deferにより正しくブラウザ側のchunkが分割されていることが分かります。

hydrate onの挙動

hydrate on immediate

他のすべてのHydrationトリガーの設定をしていない(つまり、最速でHydrationされる)コンテンツのレンダリングが完了したらすぐにHydrationをします。

hydrate on idle

ブラウザがアイドル状態になった時にHydrationをします。(requestIdleCallbackに基づく)

hydrate on timer

指定された期間後にHydrationをします。ここでは5秒後にしています。

hydrate on interaction

要素に対するユーザーのclickまたはkeydownイベントでHydrationをします。

hydrate on hover

mouseoverおよびfocusinイベントを通じてマウスがHoverした時にHydrationをします。

hydrate on viewport

指定されたコンテンツがビューポートに入った時にHydrationをします。(Intersection Observer APIに基づく)

hydrate whenの挙動

指定した条件式が真になった時にHydrationをします。

hydrate neverの挙動

Hydrationをせず、静的コンテンツと同じになります。

おわりに

サーバーとクライアントでアプリケーション全体をレンダリングしていたDestructive Hydration(破壊的ハイドレーション)の時代から見ると、Incremental Hydrationという機能はとても進化していることが分かりました。
実際に既存のアプリケーションに導入するのは、つまづくポイントがあるかもしれませんが、SSRやPrerenderingをする時には積極的に有効にしていきたいですね。

サンプルアプリケーションとコードは次のリンクで公開しています。
興味があれば、ぜひ確認してみてください。

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

Angularアドベントカレンダー2025、明日は@tminasenさんです。

Voicyテックブログ

Discussion