SSR and Hydration for Angular at Scale
"Add SSR" usually means one of two very different things: server-side rendering a page on every request, or prerendering it once at build time and serving static HTML forever after. For an app where the content doesn't depend on the logged-in user — marketing pages, docs, a portfolio site like this one — prerendering wins on every axis that matters: there's no server-side render cost per request, hosting is trivial static file serving, and there's no live-SSR failure mode to monitor in production.
Hydration is the part that actually needs care
Whichever rendering mode you pick, the client still needs to pick up the server-rendered DOM and make it interactive without a visible flash or re-render. Angular's hydration does this by matching the DOM the server produced against what the client would have rendered, then attaching event listeners instead of tearing it down and rebuilding. A few things reliably break this:
- Reading
window/document/localStoragewithout anisPlatformBrowser()guard. These don't exist during the server render, so an unguarded read throws — or worse, silently returns something the client then has to reconcile. - Client-only state that changes the server-rendered output. A theme toggle that reads
localStorageis the classic case: if you branch markup on it directly, the server renders one state, the client often has a different one, and hydration has to patch the mismatch instead of just claiming the existing nodes. The fix: set the attribute that controls it via CSS before Angular even boots (an inline script inindex.html), so the visual state is correct pre-hydration regardless of what the signal says for that first tick. - Non-deterministic content —
Math.random(),new Date()used for display, locale-dependent formatting that differs between the Node runtime and the browser.
@defer changes the hydration story too
Deferrable views mean part of the tree isn't rendered — or hydrated — until its trigger fires
(on viewport, on interaction, on idle). That's a real performance win: less JS parsed and
hydrated on first load. The trade-off is that you need a @placeholder sized close to the real
content's height, or you get layout shift when the deferred block swaps in — which quietly
undoes the Core Web Vitals win you were going for.
The result worth measuring
Done right, a prerendered Angular site ships close to static-site-generator levels of initial load performance while staying a real Angular application underneath — signals, standalone components, and router included — not a special "static mode" subset of the framework.