Lab 25: Server-Side Rendering (SSR)
π Resourcesβ
π Starter Codeβ
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.
π‘ How SSR Worksβ
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
π Instructionsβ
Step 1: Add SSR to the Projectβ
ng add @angular/ssr
This schematic does all the heavy lifting. It:
- Installs
@angular/ssrandexpress - 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.jsonwith"server"and"ssr"build options - Adds a
serve:ssrscript topackage.json
Step 2: Review the Server Configβ
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);
Angular maintains two separate configs: app.config.ts for the browser and app.config.server.ts for the server. The server config uses mergeApplicationConfig to inherit all browser providers, then adds provideServerRendering() on top.
This separation matters: browser-only providers like provideStoreDevtools() (NgRx DevTools) stay in app.config.ts only β they must never appear in app.config.server.ts, where there is no browser environment to attach to.
Step 3: Guard Browser-Only Codeβ
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β
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();
});
}
}
HTTP requests made with HttpClient during SSR are automatically transferred to the browser via Angular's state transfer mechanism β the browser does not re-fetch them. This works automatically when you use provideHttpClient(withFetch()) in your app config. No extra code needed for standard API calls.
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(),
],
};
Step 6: Build and Run the SSR Serverβ
ng build
This generates two output folders inside dist/<project-name>/:
browser/β static assets served to the clientserver/β Node.js server bundle
Tip: The exact dist path matches the
outputPathinangular.json. Verify withls dist/.
Run the SSR server:
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
Step 7: Verify SEO Improvementβ
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.
β Classroom Validationβ
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.