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.
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/ssrsets 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 withafterNextRender.
The code
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,documentorlocalStoragedirectly in components that render on the server; useafterNextRenderor injectDOCUMENT. - 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); useHttpClientorTransferStateexplicitly. - 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.