Skip to main content

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 providers
  • src/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

  1. Open http://localhost:4000
  2. View page source (Ctrl+U)
  3. The page HTML should contain your rendered content — not just <app-root></app-root>

Common Pitfalls

ProblemFix
window is not definedGuard with isPlatformBrowser or afterNextRender
Data fetched twiceUse TransferState
Hydration mismatchEnsure server and client render the same HTML
localStorage crashMove storage access behind platform check