Modern Angular: Signals, Standalone Components and SSR · Routing, State, Performance and SSR

Server-Side Rendering, Hydration and Transfer Cache

Render Angular on the server, hydrate it in the browser without re-rendering, reuse server HTTP responses with the transfer cache, and hydrate @defer blocks incrementally.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A server render of a shop page: the server fetches products from an API (a real local HTTP server), renders the HTML, adds hydration annotations, embeds the HTTP response in the transfer cache so the browser doesn't fetch it again, and marks a @defer (hydrate on viewport) block for incremental hydration.

The idea

  • SSR sends HTML first (fast first paint, good for search engines); the browser then hydrates: Angular reuses the server DOM instead of re-creating it (provideClientHydration()).
  • HTTP transfer cache: GET responses made during SSR are serialized into the page (<script id="ng-state">) and reused during hydration.
  • Incremental hydration: @defer (hydrate on viewport | interaction | idle) sends the HTML but delays loading and hydrating that block's code until the trigger.
  • With the Angular CLI, ng add @angular/ssr sets up the Express server and per-route render modes (RenderMode.Prerender, Server, Client).
  • Server code can't use browser globals (window, localStorage); guard browser-only work with afterNextRender.

The code

Rendering on the server with transfer cache and hydration
import { createServer } from "node:http";
import { Component, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { HttpClient, provideHttpClient, withFetch } from "@angular/common/http";
import { BootstrapContext, bootstrapApplication, provideClientHydration, withIncrementalHydration } from "@angular/platform-browser";
import { provideServerRendering, renderApplication } from "@angular/platform-server";

// A stand-in for your backend API.
const api = createServer((req, res) => {
  console.log(`api: ${req.method} ${req.url}`);
  res.setHeader("content-type", "application/json");
  res.end(JSON.stringify([{ id: 1, name: "Mug" }, { id: 2, name: "Teapot" }]));
});
await new Promise<void>((resolve) => api.listen(4300, "127.0.0.1", resolve));

@Component({
  selector: "app-root",
  template: `
    <h1>Shop</h1>
    <ul>@for (p of products(); track p.id) { <li>{{ p.name }}</li> }</ul>
    @defer (hydrate on viewport) { <section>Reviews: 4.8 stars</section> }
  `,
})
class App {
  products = toSignal(inject(HttpClient).get<{ id: number; name: string }[]>("http://127.0.0.1:4300/api/products"), { initialValue: [] });
}

const bootstrap = (context: BootstrapContext) =>
  bootstrapApplication(App, {
    providers: [
      provideServerRendering(),
      provideClientHydration(withIncrementalHydration()),
      provideHttpClient(withFetch()),
    ],
  }, context);

const html = await renderApplication(bootstrap, {
  document: "<!doctype html><html><head><title>Shop</title></head><body><app-root></app-root></body></html>",
  url: "/",
});
api.close();

const body = html.match(/<app-root[\s\S]*?<\/app-root>/)![0];
console.log("server HTML:", body);
const state = JSON.parse(html.match(/<script id="ng-state" type="application\/json">([\s\S]*?)<\/script>/)![1]);
for (const [key, value] of Object.entries<any>(state)) {
  if (value?.u) console.log(`transfer cache: GET ${new URL(value.u).pathname} -> ${value.s}, ${value.b.length} items`);
  else console.log(`hydration data: ${key}`);
}

Output

api: GET /api/products
server HTML: <app-root ng-version="22.2.3" ngh="0" ng-server-context="other"><h1>Shop</h1><ul><li>Mug</li><li>Teapot</li><!----></ul><!----><section>Reviews: 4.8 stars</section><!--ngh=d0--></app-root>
transfer cache: GET /api/products -> 200, 2 items
hydration data: __nghData__
hydration data: __nghDeferData__

Run this on your computer — see “Get Started” for setup.

The HTML already contains the products, so users and crawlers see content before any JavaScript runs. ngh attributes and comments let the browser reuse this DOM, __nghDeferData__ describes the deferred block for incremental hydration, and the transfer cache entry means the browser won't call the products API again during hydration.

Common mistakes

  • Using window, document or localStorage directly in components that render on the server; use afterNextRender or inject DOCUMENT.
  • Different output on server and client (random values, Date.now() in templates), causing hydration mismatches.
  • Fetching data in the browser again after SSR because requests go through something other than HttpClient (or aren't GET); use HttpClient or TransferState explicitly.
  • SSR for authenticated, per-user dashboards that gain nothing from it; use client rendering for those routes.

Key points

  • SSR sends HTML first; hydration reuses it in the browser.
  • The HTTP transfer cache avoids duplicate requests during hydration.
  • Incremental hydration delays code for @defer blocks until they're needed.

Further reading: Server-side and hybrid rendering · Hydration · Incremental hydration

Practice questions

What does hydration do in Angular?

Answer

It reuses the DOM rendered on the server, attaching event listeners and component state, instead of destroying and re-rendering it in the browser.

What is the HTTP transfer cache?

Answer

GET responses made with HttpClient during server rendering are serialized into the page and reused during hydration, so the browser doesn't repeat those requests.

What does @defer (hydrate on viewport) change?

Answer

The block is rendered on the server, but its JavaScript is loaded and hydrated only when it enters the viewport (incremental hydration).

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?