Skip to 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.

Terminal window
ng add @angular/ssr

Generated files:

  • src/app/app.config.server.ts — server providers
  • src/server.ts — Express entry point
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);
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideClientHydration(), // Avoids flicker during hydration
],
};

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();
});
}
}

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))
);
}
}
Terminal window
# 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
  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>
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