Week 21: SSR, Hydration & Deployment

The last week of Phase 5 takes your app from ng serve on your machine to a real, server-rendered URL the public can visit. Every optimization from Week 20 matters more once real users — on real networks — are the ones waiting for it to load.

Phase 5 of 7 Week 21 of 26 ~4–5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Take an app from local dev to a deployed, server-rendered build
  • Diagnose and fix a hydration mismatch instead of just silencing the warning
  • Know what to watch once an app is actually live

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.

terminal — adding SSR to an existing app
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.

app.config.server.ts
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.

app.config.ts
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.

a classic mismatch source
// 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.

app.config.ts
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withIncrementalHydration()),
  ],
};
product-page.html
@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.

angular.json (excerpt)
{
  "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.

terminal — building for production
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 NgOptimizedImage guidance), 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

Hands-on

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:

  1. Run ng add @angular/ssr on your dashboard app, and confirm it renders server-side (view page source — real content should be present, not an empty shell).
  2. Enable provideClientHydration(withEventReplay()), and fix every hydration mismatch warning in the console — not by suppressing them, but by moving the offending logic into afterNextRender().
  3. Convert at least one section (a comments area, a rarely-used panel) to incremental hydration with an appropriate trigger.
  4. Set realistic build budgets in angular.json and confirm your current build passes them.
  5. 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.
  6. Set up basic error tracking (a free tier is fine) and confirm it captures a deliberately-thrown test error.
Hint

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?

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?

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?

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?

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.