<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap"> Skip to content

Angular SSR vs CSR for SaaS Applications

Rendering strategy is a per-route decision, not a per-app one. Marketing pages, dashboards and documentation each want something different.

Ankit Narang4 min read

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

StrategyHTML fromBest forCost
CSREmpty shell + JSAuthenticated dashboardsNothing meaningful to crawl; slow first paint
SSRServer, per requestPersonalised public pagesServers to run, cache and scale
SSG / prerenderBuild timeMarketing, docs, blogRebuild to publish
Incremental hydrationBuild or server, JS deferredContent-heavy pages with interactive partsNewer, 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.

tsplatform.ts
// 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.

tshome.page.ts
@defer (hydrate on viewport) {
  <app-scale-lab />
}
@defer (hydrate on viewport) {
  <app-reliability />
}

Enable it at bootstrap:

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

tsapp.config.ts
provideZonelessChangeDetection()

Two consequences worth knowing before you flip it:

  • Every component wants OnPush and 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 setTimeout without signals will need an explicit ChangeDetectorRef.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.

MetricImproved byNot improved by
First Contentful PaintPrerender / SSRZoneless
Largest Contentful PaintPrerender + font loadingDeferring JS
Interaction to Next PaintLess JS, less work on interactionSSR
Total Blocking TimeDeferred hydration, zonelessSSR alone
SEO indexationReal HTML in the responseAnything 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:

  1. 01Prerender the public surface. Marketing, pricing, blog, docs, changelog. Static output on a CDN, no server tier, nothing to scale or page anyone about.
  2. 02CSR the authenticated app. No SEO value in it, and every response is personalised anyway.
  3. 03Reserve SSR for the narrow case of public, per-request content — shared links, generated documents.
  4. 04Use incremental hydration on the long content pages, where most of the JavaScript is for things below the fold.
  5. 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.

Have a system like this to build?

I take products from architecture to production — and I will tell you honestly if I am not the right fit.

Start a project