1. Server-Side Rendering with Angular
Without SSR, a browser downloads a nearly-empty HTML shell, then waits for JavaScript to download, parse, and run before anything appears. SSR renders the initial HTML on the server instead — the user sees real content immediately, before any JavaScript has run at all.
ng add @angular/ssr
This schematic generates a server entry point and wires up
provideServerRendering(), which tells Angular to render your component
tree to a string of HTML on the server for each incoming request, before any client-side
JavaScript is involved.
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/ssr';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
Notice this merges with your existing appConfig from Week 3 rather
than replacing it — the same providers, routes, and services run on the server as run in
the browser, with one addition specific to server rendering.
2. Hydration & Avoiding Mismatches
After the server-rendered HTML reaches the browser, Angular needs to "wake it up" — attach event listeners, restore signal state, make it interactive — without throwing away and re-rendering the DOM the server already produced. This process is hydration.
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()), // captures clicks during hydration, replays them after
],
};
A hydration mismatch happens when the HTML the client renders doesn't match what the server produced — Angular logs a warning and, in the mismatched section, falls back to discarding and re-rendering that DOM, losing the performance benefit SSR was meant to provide.
// BAD: Date.now() produces a DIFFERENT value on the server vs. the client
<p>Loaded at: {{ Date.now() }}</p>
// BAD: directly reading `window` during rendering -- doesn't exist on the server at all
<p>Screen width: {{ window.innerWidth }}</p>
The fix is the same in both cases: compute anything environment-dependent
after the component has rendered — inside afterNextRender() —
rather than directly in a template expression that runs during the render itself, on
both server and client.
3. Incremental Hydration & @defer on the Server
Full hydration processes the entire page at once. Incremental hydration
extends Week 11's @defer blocks to hydration itself — a deferred block can
render as static HTML from the server, then hydrate independently, later, on its own
trigger.
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration()),
],
};
@defer (hydrate on interaction) {
<app-comments-section [productId]="product().id" />
} @placeholder {
<div class="comments-skeleton"></div>
}
hydrate on interaction means the comments section renders as plain HTML
immediately (good for SEO and first paint) but doesn't cost any JavaScript execution
until a user actually interacts with it — a below-the-fold, rarely-interacted-with
section no longer competes for the browser's attention during initial load.
4. Build Budgets & Production Configuration
angular.json lets you set size budgets per bundle — the build
warns or fails once a bundle crosses a threshold,
catching bundle bloat (an accidentally-eager import, from Week 20) before it ships
rather than after.
{
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
]
}
Treat a budget crossing as a real signal worth investigating, not noise to raise the threshold past — it's specifically designed to catch the moment your bundle started growing for a reason nobody intended.
5. Deploying to a Hosting Provider
An SSR app needs a Node.js server to run — different from a pure static app, which is just files served by any web host.
ng build
# Output:
# dist/my-app/browser/ -- static assets, served directly to the client
# dist/my-app/server/ -- the Node.js server that renders on each request
- Static hosting (no SSR, or pre-rendered routes) — any CDN or static host works: Netlify, GitHub Pages, Cloudflare Pages.
- SSR hosting — needs a Node.js runtime: Vercel, a container platform (Cloud Run, Fly.io), or your own Node server running
dist/my-app/server/server.mjs.
A middle ground worth knowing: prerendering (ng build
--prerender) runs SSR once, at build time, for routes with no per-request dynamic
data — producing pure static HTML for those routes, deployable to any static host with
none of SSR's runtime server requirement.
6. Monitoring a Live App
Once real users are involved, "does it work on my machine" stops being a useful question. Two categories worth tracking from day one:
- Error tracking — a service (Sentry and similar) that captures uncaught exceptions in production, with the stack trace and enough context to actually reproduce the bug — errors you'd otherwise never know happened.
- Core Web Vitals — Google's standardized real-user performance metrics: LCP (Largest Contentful Paint — how fast the main content appears), CLS (Cumulative Layout Shift — how much content jumps around, directly tied to Week 20's
NgOptimizedImageguidance), and INP (Interaction to Next Paint — how responsive the app feels to input).
These aren't lab measurements from your own machine — they're collected from actual visitors, on their actual devices and networks, which is precisely why they can reveal problems a fast development laptop never would.
7. Hands-on Exercise
Add SSR to your dashboard app and deploy it
Take the app you've been building since Week 9 all the way to a live, public URL.
Requirements:
- Run
ng add @angular/ssron your dashboard app, and confirm it renders server-side (view page source — real content should be present, not an empty shell). - Enable
provideClientHydration(withEventReplay()), and fix every hydration mismatch warning in the console — not by suppressing them, but by moving the offending logic intoafterNextRender(). - Convert at least one section (a comments area, a rarely-used panel) to incremental hydration with an appropriate trigger.
- Set realistic build budgets in
angular.jsonand confirm your current build passes them. - Deploy the built app to a real hosting provider that supports Node.js SSR, and confirm the live URL actually works — including a hard refresh on an inner route, which is the case most likely to reveal a routing/SSR misconfiguration.
- Set up basic error tracking (a free tier is fine) and confirm it captures a deliberately-thrown test error.
If a hard refresh on an inner route (like /projects/42) 404s while navigating there from the home page works fine, the issue is almost always server-side routing configuration — the server needs to know to run your Angular app's router for that path too, not just serve a literal file.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does hydration actually do to server-rendered HTML, and why doesn't Angular just discard it and re-render from scratch?
What does hydration actually do to server-rendered HTML, and why doesn't Angular just discard it and re-render from scratch?
Hydration attaches event listeners and restores application state to the existing server-rendered DOM, making it interactive. Discarding and re-rendering from scratch would throw away the entire performance benefit of SSR — the user would briefly see fast, static content, then experience a visible flash as it's replaced by the client-rendered version.
Q2
Why does reading Date.now() directly in a template cause a hydration mismatch?
Why does reading Date.now() directly in a template cause a hydration mismatch?
The server renders the template at request time, and the client renders (or hydrates) it moments later — Date.now() returns a genuinely different value each time it's called, so the server's HTML and the client's expected output disagree. Angular detects this and cannot cleanly hydrate that portion of the DOM.
Q3
What does hydrate on interaction actually change about when a deferred block becomes interactive?
What does hydrate on interaction actually change about when a deferred block becomes interactive?
The block still renders as real HTML from the server immediately (so it's visible and indexable right away), but Angular delays running the JavaScript needed to make it interactive until the user actually interacts with it — rather than hydrating it eagerly along with the rest of the page on load.
Q4
Why are Core Web Vitals collected from real users instead of measured once on a developer's machine?
Why are Core Web Vitals collected from real users instead of measured once on a developer's machine?
A developer's machine typically has a fast connection, a powerful CPU, and a warm cache — none of which represent the actual range of devices and networks real visitors use. Real-user measurement captures the experience of someone on a slow connection or an older phone, which a controlled lab test on fast hardware would never surface.