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
Section titled “Add SSR”ng add @angular/ssrGenerated files:
src/app/app.config.server.ts— server providerssrc/server.ts— Express entry point
Server Config
Section titled “Server Config”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)
Section titled “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
Section titled “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)
Section titled “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
Section titled “Build and Run”# Build browser + server bundlesng build
# Start the SSR servernode dist/my-app/server/server.mjsOutput structure:
dist/my-app/├── browser/ ← Static assets└── server/ └── server.mjs ← Node serverVerify SSR is Working
Section titled “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
Section titled “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 |

