Built — Describes systems I have built and run. Specifics are generalised where client work is involved.
"Should we use SSR?" is the wrong question, because it assumes one answer for the whole application. A SaaS product is at least three different applications wearing one domain: a marketing site, a logged-in dashboard, and documentation. Each wants a different rendering strategy.
#Four strategies, not two
| Strategy | HTML from | Best for | Cost |
|---|---|---|---|
| CSR | Empty shell + JS | Authenticated dashboards | Nothing meaningful to crawl; slow first paint |
| SSR | Server, per request | Personalised public pages | Servers to run, cache and scale |
| SSG / prerender | Build time | Marketing, docs, blog | Rebuild to publish |
| Incremental hydration | Build or server, JS deferred | Content-heavy pages with interactive parts | Newer, fewer known patterns |
The last one is the interesting development and the least understood.
#Route-by-route, for a typical SaaS
/ ── prerender public, static, must rank /pricing ── prerender public, static, must rank /blog, /blog/:slug ── prerender public, static, must rank /docs/** ── prerender public, large, must rank /changelog ── prerender rebuilt on publish /app/** ── CSR authenticated; SEO irrelevant /app/reports/:id ── CSR personalised, behind a login /share/:token ── SSR public but per-request content /invoice/:id/pdf ── SSR generated per request
Prerender the public surface, CSR the product, SSR only what is genuinely both public and per-request.
The common mistake is turning on SSR globally and paying for it on /app/**, where nobody is crawling, every response is personalised so nothing caches, and you have added a server tier that can now be the thing that goes down.
#When SSR is actually the wrong answer
For an authenticated dashboard. Server-rendering a page that requires a session means fetching the user's data on the server, sending HTML nobody can cache, then hydrating anyway. You get a slower time to first byte and no SEO benefit, because the crawler cannot log in.
When the data is behind an API you do not control. SSR turns every page view into a server-side API call. Your rendering latency becomes your slowest upstream dependency, and a rate limit on that API becomes a rendering outage.
When your team has not budgeted for two runtimes. SSR means the same code runs in Node and in a browser. Every window, document, localStorage and IntersectionObserver reference becomes a platform check. That discipline is learnable, and it is not free.
// This runs on the server too. Reaching for `window` here crashes the render.
export class ThemeService {
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
get stored(): string | null {
if (!this.isBrowser) return null;
try {
return localStorage.getItem('theme');
} catch {
return null; // private mode, blocked storage
}
}
}#Incremental hydration: the part worth learning
Traditional hydration is all-or-nothing. The server sends complete HTML, then the client downloads the JavaScript for the entire page and re-walks the whole tree before anything is interactive. A long content page pays for interactivity it may never use.
Angular's @defer (hydrate on viewport) decouples the two. The content is server-rendered — present in the HTML, crawlable, painted immediately — while the JavaScript for that block is fetched and hydrated only when the reader reaches it.
@defer (hydrate on viewport) {
<app-scale-lab />
}
@defer (hydrate on viewport) {
<app-reliability />
}Enable it at bootstrap:
provideClientHydration(withIncrementalHydration())The distinction that matters: @defer (on viewport) renders a placeholder on the server and the real content only after the trigger — bad for SEO. @defer (hydrate on viewport) renders the real content on the server and defers only the hydration. Same directive, opposite consequences for crawlers.
#Zoneless is the other half
zone.js is roughly 35 kB before compression and works by monkey-patching every asynchronous browser API so Angular can guess when something changed. Signals make the guessing unnecessary — a signal write tells the framework exactly what changed.
provideZonelessChangeDetection()Two consequences worth knowing before you flip it:
- Every component wants
OnPushand signals. Mutating a plain object property and expecting the view to update will not work. This is a correctness improvement disguised as a constraint. - Third-party libraries that rely on zone patching may need a nudge. Most modern ones are fine; older ones that mutate state from a
setTimeoutwithout signals will need an explicitChangeDetectorRef.markForCheck().
Combined with prerendering, the result is a page whose interactive parts arrive as the reader scrolls, on a framework that is not patching the event loop.
#Measuring the right thing
Rendering strategy affects some metrics and not others, and it is easy to optimise the wrong one.
| Metric | Improved by | Not improved by |
|---|---|---|
| First Contentful Paint | Prerender / SSR | Zoneless |
| Largest Contentful Paint | Prerender + font loading | Deferring JS |
| Interaction to Next Paint | Less JS, less work on interaction | SSR |
| Total Blocking Time | Deferred hydration, zoneless | SSR alone |
| SEO indexation | Real HTML in the response | Anything client-only |
SSR alone does not improve Total Blocking Time. It can make it worse: the page paints early, so the window in which a user can try to interact — and find the page unresponsive because hydration is still running — gets longer. That gap is exactly what incremental hydration closes.
#A practical recommendation
For most SaaS products:
- 01Prerender the public surface. Marketing, pricing, blog, docs, changelog. Static output on a CDN, no server tier, nothing to scale or page anyone about.
- 02CSR the authenticated app. No SEO value in it, and every response is personalised anyway.
- 03Reserve SSR for the narrow case of public, per-request content — shared links, generated documents.
- 04Use incremental hydration on the long content pages, where most of the JavaScript is for things below the fold.
- 05Go zoneless if you are on a recent Angular and already using signals.
The decision is per route. Any framework or article that gives you one answer for the whole application is answering a simpler question than the one you have.
Key takeaways
Choose rendering per route. A SaaS product almost always needs more than one strategy. Prerender anything public; a logged-in dashboard gains nothing from SSR. Incremental hydration gets you server-rendered HTML without shipping all the JavaScript at once. Going zoneless removes a real chunk of bundle and makes change detection predictable.