
Angular SSR Explained: Why It Exists and What CSR Can't Do

D. Rout
August 19, 2026 7 min read
On this page
The problem
Load a default Angular app on a throttled connection and open "View Page Source." You'll see <app-root></app-root> and not much else. Everything a user sees, and everything a search engine or social-media crawler reads, only exists after the browser downloads, parses, and executes your JavaScript bundle. On a fast desktop that gap is milliseconds. On a mid-range phone over 4G, it can be seconds of blank white screen.
Server-Side Rendering (SSR) closes that gap by running your Angular app once on the server for each request and sending back real, populated HTML. This post explains why that matters and exactly how it differs from the client-side Angular you already know. Part 2 builds a working hybrid-rendered app step by step, in this companion repo: angular-ssr-hybrid-rendering-demo.
Prerequisites
- Comfortable with standalone Angular components, the Router, and
HttpClient - Angular 18+ mental model (signals, `@defer`, `@if`/`@for` control flow)
- Node.js 20.11+ installed locally if you want to follow along with commands
- No prior SSR or Angular Universal experience required, this post starts from zero
1. What "client-side rendering" actually means
In a standard Angular CSR app, the server's only job is to hand over a nearly empty HTML shell plus a bundle of JavaScript:
<!-- what the server sends for EVERY route in a pure CSR app -->
<!doctype html>
<html>
<head><title>My App</title></head>
<body>
<app-root></app-root>
<script src="main.js"></script>
</body>
</html>
The browser then has to download main.js, boot Zone.js or the zoneless scheduler, run change detection, execute your route resolvers and HTTP calls, and finally paint real content. Every one of those steps sits between the network request and something the user can read. That sequence is the entire story of why CSR apps feel slow on first load and invisible to simple crawlers.
2. What SSR changes about the request lifecycle
With SSR, the same Angular application runs twice: once on the server (using @angular/platform-server under the hood, wired up through @angular/ssr) and once in the browser. On the server, Angular renders your components to a full HTML string, including the data your components fetched, and sends that down as the initial response:
<!-- what the server sends for an SSR route -->
<!doctype html>
<html>
<head><title>Ultrawide Monitor - My Shop</title></head>
<body>
<app-root>
<h1>Ultrawide Monitor</h1>
<p>34" curved, 144Hz, great for split-pane coding.</p>
<p><strong>$749</strong></p>
</app-root>
<script src="main.js"></script>
</body>
</html>
The user sees real content the instant the HTML arrives, before any JavaScript has run. The JavaScript bundle still loads afterward, but its job changes from "build the page" to "make the already-visible page interactive." That second step is called hydration, covered in section 5.
3. The three render modes Angular gives you per route
Angular's SSR package doesn't force an all-or-nothing choice. In app.routes.server.ts you declare a renderMode per route, and you can mix all three freely across one app:
// src/app/app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
// Client-Side Rendering: no per-user data, ship a CSR shell
renderMode: RenderMode.Client,
},
{
path: 'about',
// Static Site Generation: build once, serve from a CDN forever
renderMode: RenderMode.Prerender,
},
{
path: 'products/:id',
// Server-Side Rendering: fresh HTML computed per request
renderMode: RenderMode.Server,
},
];
That file is wired into your server bootstrap with provideServerRendering and withRoutes:
// src/app/app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(withRoutes(serverRoutes))],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
This is the piece most tutorials skip: SSR isn't a single global switch, it's a routing decision you make per page, based on how volatile that page's content is and who needs to read it before JavaScript runs.
4. Deciding which mode fits which route
RenderMode.Server(SSR): the page needs current, per-request data (a logged-in dashboard, live pricing, a product detail page) and it needs to be crawlable or fast on first paint.RenderMode.Prerender(SSG): the content is the same for every visitor and changes rarely (marketing pages, docs, an About page). Build it once, serve it from cache.RenderMode.Client(CSR): the page is behind auth, has no SEO value, or is so interaction-heavy that server rendering adds cost without benefit (a canvas-based editor, for instance).
Most real apps end up as a mix of all three, which Angular calls hybrid rendering.
5. Hydration: how the client picks up what the server started
Sending pre-rendered HTML solves the "blank screen" problem, but on its own it creates a new one: without help, Angular would throw away that server-rendered DOM and rebuild it from scratch in the browser, causing a visible flicker and wasted work. Hydration prevents that by reusing the existing DOM and attaching event listeners to it instead of re-rendering:
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
// withEventReplay() captures clicks/input that happen before hydration
// finishes and replays them once listeners are attached, so early
// interactions aren't silently dropped.
provideClientHydration(withEventReplay()),
],
};
As of Angular 22, incremental hydration (hydrating individual @defer blocks only when they're needed, e.g. on viewport) ships enabled by default alongside provideClientHydration(). Part 2 walks through a concrete @defer (hydrate on viewport) example on a comments section.
6. What SSR actually unlocks
- SEO and link previews: crawlers and social-media unfurlers read the initial HTML response. If that response is empty, your rich metadata and content may as well not exist.
- Faster Largest Contentful Paint (LCP): users see real content on arrival instead of waiting for a JS bundle to download and execute, which matters most on slow networks and low-end devices.
- Lower Cumulative Layout Shift (CLS): hydration reuses the server DOM in place, so nothing pops into existence and reflows the page.
- Resilience on flaky connections: if the JS bundle is slow to load or fails, the user still has readable content rather than a blank shell.
- Consistent behavior for non-JS clients: some corporate proxies, accessibility tools, and older devices handle plain HTML far better than a JS-driven app.
Reference: CSR vs. SSR vs. SSG at a glance
| Dimension | CSR (RenderMode.Client) |
SSR (RenderMode.Server) |
SSG (RenderMode.Prerender) |
|---|---|---|---|
| When HTML is built | In the browser, after JS loads | On the server, per request | Once, at build time |
| First paint speed | Slowest on weak devices/networks | Fast, scales with server response time | Fastest, often served from CDN |
| Server compute per request | None | One render per request | None after build |
| Good for per-user/live data | Yes | Yes | No |
| SEO / crawler friendly | Weak without extra work | Strong | Strong |
| Best fit | Auth-gated, highly interactive views | Product pages, dashboards with fresh data | Marketing pages, docs, rarely-changing content |
What's next
Part 2 of this series builds a real hybrid-rendered app that uses all three modes side by side, plus:
- Wiring up an
AngularAppEngine+ Express server that also serves a small JSON API - An SSR product-detail route that fetches data server-side and transfers it to the client without a duplicate HTTP call
- Incrementally hydrating a comments block with `@defer (hydrate on viewport)`
- A deployment checklist covering caching headers,
maxResponseBodySize, and common hydration mismatch errors
Further reading
- Angular SSR guide (angular.dev)
- Hydration guide (angular.dev)
- Incremental hydration guide (angular.dev)
- Rendering on the Web (web.dev)
- Core Web Vitals (web.dev)
Closing
SSR isn't a wholesale replacement for the Angular you already know, it's an additional rendering strategy you apply route by route, wherever a blank first paint or an invisible crawler actually costs you something. In Part 2, we'll put every concept from this post into a working app you can clone and run yourself: github.
Read next
Comments (0)
Join the conversation
Sign in to leave a comment on this post.
No comments yet. to be the first!