Introduction to SSR
By default an Angular application is a client-side rendered (CSR) single-page app: the browser downloads a near-empty HTML shell, then JavaScript boots the framework and paints the UI. Server-side rendering (SSR) flips the first step around — Angular runs on the server, produces fully-formed HTML, and ships that to the browser so users see real content immediately. This dramatically improves perceived performance and makes your pages legible to crawlers and link previews. Modern Angular (17+) bakes SSR directly into the framework and CLI, so it is no longer a bolt-on library.
Why server-side rendering matters
A pure CSR app has two costs that SSR addresses. First, time-to-content: the user stares at a blank screen until the JavaScript bundle is parsed, the app bootstraps, and any data is fetched. On slow networks or low-end devices this can take seconds. Second, discoverability: many crawlers, social-media unfurlers, and older bots either do not execute JavaScript or do so unreliably, so a CSR app can look empty to them.
With SSR, the server returns HTML that already contains your headings, text, and meta tags. The browser paints it instantly, and the JavaScript later “hydrates” that markup to make it interactive. The result is faster First Contentful Paint, better Core Web Vitals, and reliable SEO.
| Concern | CSR only | With SSR |
|---|---|---|
| First Contentful Paint | After JS bootstraps | Immediate (server HTML) |
| SEO / crawlers | Risky | Reliable, full markup |
| Social link previews | Often empty | Correct title/description |
| Time to Interactive | Single phase | After hydration |
| Server cost | Static hosting | Needs a Node server |
Note: SSR is not the same as a static site. The HTML is generated per request on a Node.js server (or per build for prerendered routes). Plan for the operational cost of running that server.
How modern Angular SSR works
The SSR flow has three stages:
- Render on the server. A Node process runs your Angular app, executes components, resolves data, and serializes the resulting DOM to an HTML string.
- Send HTML to the browser. The user sees content right away — before any application JavaScript has run.
- Hydrate on the client. Angular boots in the browser, reuses the existing server-rendered DOM instead of throwing it away, and wires up event listeners and change detection.
The key piece is non-destructive hydration, enabled with provideClientHydration(). Without it, Angular would discard the server markup and re-render from scratch (a “flicker”); with it, Angular adopts the existing nodes.
// app.config.ts (client configuration)
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(withEventReplay()),
],
};
On the server side, Angular adds a matching configuration that is merged with the client one:
// 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);
A standalone component looks identical whether it renders on the server or the client — there is no special “server component” syntax. Modern control flow works the same in both environments:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-product',
standalone: true,
template: `
<h1>{{ name() }}</h1>
@if (inStock()) {
<p>In stock — ships today.</p>
} @else {
<p>Currently unavailable.</p>
}
`,
})
export class ProductComponent {
name = signal('Mechanical Keyboard');
inStock = signal(true);
}
Adding SSR with the CLI
You rarely wire this up by hand. The Angular CLI scaffolds the server entry point, the server config, and an Express handler for you:
ng add @angular/ssr
To build and run an SSR app locally:
ng build
node dist/my-app/server/server.mjs
Output:
Node Express server listening on http://localhost:4000
When you request a page, the server returns hydrated-ready HTML. You can confirm SSR is working by viewing the raw response — your content should already be present:
curl -s http://localhost:4000/ | grep "<h1>"
Output:
<h1>Mechanical Keyboard</h1>
If the <h1> were missing from the curl output, the page was rendered only on the client — a sign SSR is not active for that route.
What changes for your code
Because components now run on a server too, the global window, document, and localStorage objects do not exist during the server render. Guard browser-only logic with isPlatformBrowser and run DOM work in lifecycle hooks that only fire in the browser, such as afterNextRender.
import { Component, inject, PLATFORM_ID, afterNextRender } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({ selector: 'app-theme', standalone: true, template: '' })
export class ThemeComponent {
private platformId = inject(PLATFORM_ID);
constructor() {
afterNextRender(() => {
// Runs only in the browser, never on the server.
const saved = localStorage.getItem('theme') ?? 'light';
document.documentElement.dataset.theme = saved;
});
if (isPlatformBrowser(this.platformId)) {
console.log('Running in the browser');
}
}
}
Gotcha: Reaching for
windowordocumentat construction time or inngOnInitwill throwReferenceError: window is not definedon the server. Always gate browser APIs behind a platform check orafterNextRender.
Best practices
- Enable
provideClientHydration()so Angular reuses server DOM instead of re-rendering and flickering. - Add
withEventReplay()so clicks that happen before hydration are captured and replayed once the app is interactive. - Never touch
window,document, orlocalStorageduring the server render; guard withisPlatformBrowserorafterNextRender. - Use Angular’s
HttpClientfor data fetching so theTransferStatecache avoids duplicate requests after hydration. - Set page titles and meta tags with
TitleandMetaservices so crawlers receive correct SEO data from the server HTML. - Prerender stable, content-heavy routes at build time to skip per-request rendering and reduce server load.