Server-Side Rendering (SSR)
Angular SSR pre-renders pages on the server before sending them to the browser, improving initial load performance and SEO.
Add SSR
ng add @angular/ssr
Generated files:
src/app/app.config.server.ts— server providerssrc/server.ts— Express entry point
Server Config
// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
Enable Hydration (app.config.ts)
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideClientHydration(), // Avoids flicker during hydration
],
};
Guard Browser-Only APIs
The server has no window, document, or localStorage. Use isPlatformBrowser or afterNextRender:
import { PLATFORM_ID, inject, afterNextRender } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export class MyComponent {
private platformId = inject(PLATFORM_ID);
// Option A: isPlatformBrowser
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
const value = localStorage.getItem('key');
}
}
// Option B: afterNextRender (preferred, Angular 16+)
constructor() {
afterNextRender(() => {
// Runs only in the browser after first render
this.initThirdPartyLib();
});
}
}
Transfer State (Avoid Double Fetching)
Without transfer state, Angular re-fetches data in the browser that the server already fetched.
import { TransferState, makeStateKey, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { of, tap } from 'rxjs';
const ALBUMS_KEY = makeStateKey<Album[]>('albums');
@Injectable({ providedIn: 'root' })
export class AlbumService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
getAlbums() {
const cached = this.transferState.get(ALBUMS_KEY, null);
if (cached) {
this.transferState.remove(ALBUMS_KEY);
return of(cached);
}
return this.http.get<Album[]>('/api/albums').pipe(
tap(data => this.transferState.set(ALBUMS_KEY, data))
);
}
}
Build and Run
# Build browser + server bundles
ng build
# Start the SSR server
node dist/my-app/server/server.mjs
Output structure:
dist/my-app/
├── browser/ ← Static assets
└── server/
└── server.mjs ← Node server
Verify SSR is Working
- Open
http://localhost:4000 - View page source (Ctrl+U)
- The page HTML should contain your rendered content — not just
<app-root></app-root>
Common Pitfalls
| Problem | Fix |
|---|---|
window is not defined | Guard with isPlatformBrowser or afterNextRender |
| Data fetched twice | Use TransferState |
| Hydration mismatch | Ensure server and client render the same HTML |
localStorage crash | Move storage access behind platform check |