Skip to content

Lab 25: Server-Side Rendering (SSR)

album-wholesale-v17-signal-pre-jest

In this lab you will enable Server-Side Rendering in the album wholesale app. SSR pre-renders pages on the server, improving initial load time and SEO. You will handle browser-only APIs safely, transfer state to avoid double fetching, and run the SSR server locally.

With Client-Side Rendering (CSR), the server sends a blank HTML shell — the browser receives <app-root></app-root> empty, then downloads and executes JavaScript to build the page.

With Server-Side Rendering (SSR), the server renders the full HTML first. The browser receives a complete page, which is faster to display and indexable by search engines. Angular then hydrates the page — it attaches event listeners and takes over without re-rendering.

CSR flow: Server → blank HTML → browser downloads JS → browser renders
SSR flow: Server renders HTML → browser displays instantly → Angular hydrates
Terminal window
ng add @angular/ssr

This schematic does all the heavy lifting. It:

  • Installs @angular/ssr and express
  • Creates src/main.server.ts — server bootstrap entry point
  • Creates server.ts — the Express HTTP server at project root
  • Creates src/app/app.config.server.ts — server-specific providers
  • Updates angular.json with "server" and "ssr" build options
  • Adds a serve:ssr script to package.json

Inspect the generated 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);

The server has no access to window, localStorage, or document. Guard browser-only code with isPlatformBrowser:

import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export class AlbumDetailComponent {
private platformId = inject(PLATFORM_ID);
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Safe to access window, localStorage, etc.
const saved = localStorage.getItem('recentAlbums');
}
}
}

Step 4: Use afterNextRender for DOM Access

Section titled “Step 4: Use afterNextRender for DOM Access”

Prefer afterNextRender (Angular 16+) over lifecycle hooks for browser-only side effects:

import { afterNextRender } from '@angular/core';
export class AlbumPlayerComponent {
constructor() {
afterNextRender(() => {
// Runs only in the browser, after first render
this.initAudioPlayer();
});
}
}

Step 5: Transfer State to Avoid Double Fetching

Section titled “Step 5: Transfer State to Avoid Double Fetching”

Without transfer state, Angular re-fetches data in the browser even though the server already fetched it. Fix this with TransferState:

import { TransferState, makeStateKey } from '@angular/core';
import { inject } from '@angular/core';
const ALBUMS_KEY = makeStateKey<Album[]>('albums');
export class AlbumService {
private transferState = inject(TransferState);
private http = inject(HttpClient);
getAlbums(): Observable<Album[]> {
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(albums => this.transferState.set(ALBUMS_KEY, albums))
);
}
}

Also enable hydration in app.config.ts:

import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideClientHydration(),
],
};
Terminal window
ng build

This generates two output folders inside dist/<project-name>/:

  • browser/ — static assets served to the client
  • server/ — Node.js server bundle

Tip: The exact dist path matches the outputPath in angular.json. Verify with ls dist/.

Run the SSR server:

Terminal window
node dist/album-wholesale-v17/server/server.mjs

Open http://localhost:4000 and check:

  • View source (Ctrl+U) — page HTML should already contain album content
  • Network tab — no extra API calls after hydration

Open the page source and confirm:

  • <title> tag contains the album name
  • Meta description is present
  • Album list HTML is visible in the source (not just <app-root></app-root>)

This confirms the page is rendered on the server before being sent to the browser.

The clearest demo of SSR working: open DevTools → Network → refresh the page and click the initial HTML document request. Look at the Response tab.

  • CSR: you see <app-root></app-root> — empty, no content
  • SSR: you see full album list HTML inside <app-root> — rendered on the server

No plugins needed. This single comparison makes the value of SSR immediately tangible.