Server-Side Rendering (SSR), Hydration & SSG
In modern enterprise web development, delivering instant initial page loads, optimal Search Engine Optimization (SEO) scores, and high Core Web Vitals (Largest Contentful Paint) requires hybrid rendering architectures. Modern Angular provides full server-side rendering capabilities via @angular/ssr, supporting Server-Side Rendering (SSR), Static Site Generation (SSG / Prerendering), Non-Destructive Hydration, and Event Replay.
┌─────────────────────────────────────────────────────────────┐
│ Angular SSR & Hydration Lifecycle │
│ │
│ 1. Client Requests URL (/products) │
│ │ │
│ ▼ │
│ 2. Node.js Server Renders Angular App via @angular/ssr │
│ ├── Fetches API data via HttpClient │
│ ├── Stores API data in TransferState cache │
│ └── Emits complete semantic HTML document │
│ │ │
│ ▼ │
│ 3. Browser Displays Server-Rendered HTML Instantly (Fast LCP)│
│ └── Event Replay captures user clicks before hydration │
│ │ │
│ ▼ │
│ 4. Client Downloads JavaScript & Non-Destructive Hydration │
│ ├── Reuses existing DOM nodes (Zero screen flicker) │
│ ├── Replays captured user events │
│ └── App becomes fully interactive SPA │
└─────────────────────────────────────────────────────────────┘
1. Enabling Angular SSR and Non-Destructive Hydration
To add SSR and hydration to an Angular application:
# Add SSR to an existing project
ng add @angular/ssr
In src/app/app.config.ts:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()),
// Enable Non-Destructive Hydration and Event Replay
provideClientHydration(
withEventReplay() // Replays clicks made before JS finished downloading
)
]
};
2. Preventing Duplicate API Calls with TransferState
Without TransferState, an application rendered on the server executes HTTP requests during SSR, sends HTML to the browser, and then the client-side JavaScript executes the exact same HTTP requests again upon bootstrap.
Angular's HttpClient automatically serializes and transfers cached GET responses from the server to the client when hydration is enabled:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-catalog',
standalone: true,
template: `
<ul>
@for (prod of products(); track prod.id) {
<li>{{ prod.name }} - \${{ prod.price }}</li>
}
</ul>
`
})
export class CatalogComponent {
private http = inject(HttpClient);
// Automatically cached on server and dehydrated to client
readonly products = toSignal(this.http.get<any[]>('/api/products'));
}
3. Resolving Hydration Mismatch Errors
A hydration mismatch occurs when the HTML generated on the server does not match the initial DOM structure generated by the client JavaScript (e.g. using new Date() or Math.random() directly in templates, or referencing window.innerWidth).
To fix hydration mismatches:
- Use
afterNextRender(() => ...)for browser-specific APIs. - Use the
ngSkipHydrationattribute on third-party DOM components that cannot be safely hydrated.
<!-- Skip hydration on third-party canvas or legacy jQuery widget -->
<div ngSkipHydration>
<canvas id="legacy-chart"></canvas>
</div>
Summary & Key Takeaways
- Angular SSR renders complete HTML on the server for maximum SEO and lightning-fast LCP.
- Non-destructive hydration attaches event listeners to existing DOM nodes without destroying and recreating them.
withEventReplay()captures user interactions during page load and replays them once JavaScript hydrates.TransferStateeliminates duplicate network requests between server and client.
Best Practices & Senior Guidance
- Always Use
withEventReplay(): EnableprovideClientHydration(withEventReplay())to ensure user clicks during initial load are never dropped. - Never Reference
windowordocumentGlobally: Wrap browser-specific API calls insideafterNextRender()or guard withisPlatformBrowser(platformId). - Use Static Site Generation (SSG) for Marketing Pages: Configure static prerendering in
angular.jsonfor content that does not change per user session.