Server-Side Rendering (SSR) improves initial load performance and SEO by rendering your Angular app on the server. Angular provides built-in SSR support with hydration for seamless client-side takeover.Why does this matter? Without SSR, your Angular app ships an empty <div> to the browser, which then downloads JavaScript, executes it, and finally renders the page. During that loading time (often 2-5 seconds on slow connections), search engine crawlers see nothing — which devastates your SEO rankings. Social media previews (sharing a link on LinkedIn or Twitter) show a blank card. And users on slower devices stare at a white screen. SSR solves all three problems by sending a fully rendered HTML page immediately.What You’ll Learn:
┌─────────────────────────────────────────────────────────────────────────┐│ Client-Side vs Server-Side Rendering │├─────────────────────────────────────────────────────────────────────────┤│ ││ Client-Side Rendering (CSR) Server-Side Rendering (SSR) ││ ─────────────────────────── ───────────────────────── ││ ││ Browser ─────────────────▶ Browser ─────────────────▶ ││ 1. Request HTML (empty) 1. Request HTML ││ 2. Download JS bundles 2. Server renders HTML ││ 3. Execute JavaScript 3. Send complete HTML ││ 4. Render application 4. Display content (fast!) ││ 5. Interactive 5. Download JS in background ││ └──── Blank screen ────┘ 6. Hydration ││ 7. Interactive ││ Time to First Paint: SLOW Time to First Paint: FAST ││ ││ Benefits of SSR: ││ ✓ Faster First Contentful Paint (FCP) ││ ✓ Better SEO (crawlers see content) ││ ✓ Social media link previews work ││ ✓ Better Core Web Vitals ││ ✓ Works without JavaScript ││ │└─────────────────────────────────────────────────────────────────────────┘
When NOT to use SSR: If your app is a fully authenticated dashboard (like an admin panel) where SEO does not matter and users are always logged in, SSR adds complexity without much benefit. The sweet spot for SSR is public-facing content: marketing pages, blog posts, e-commerce product pages, and any page you want to rank in Google or preview on social media.
# Create new project with SSR built in from the startng new my-app --ssr# Or add SSR to an existing project -- this generates server.ts,# main.server.ts, and updates angular.json automaticallyng add @angular/ssr
Browser APIs (window, document, localStorage) do not exist on the server. This is the single most common source of SSR bugs — your app works perfectly in the browser, but crashes on the server because code tries to access window.innerWidth or localStorage.getItem() during server-side rendering. The fix is straightforward: guard all browser-specific code behind platform checks.
import { Component, inject, PLATFORM_ID } from '@angular/core';import { isPlatformBrowser, isPlatformServer } from '@angular/common';@Component({...})export class HeaderComponent { private platformId = inject(PLATFORM_ID); ngOnInit() { if (isPlatformBrowser(this.platformId)) { // Only runs in browser window.addEventListener('scroll', this.onScroll); const theme = localStorage.getItem('theme'); } if (isPlatformServer(this.platformId)) { // Only runs on server console.log('Rendering on server'); } }}
These lifecycle hooks are the preferred way to run browser-only code in Angular 17+. Unlike isPlatformBrowser checks, they are guaranteed to only execute in the browser — there is no risk of accidentally running browser code on the server. Use afterNextRender for one-time initialization (chart libraries, analytics scripts) and afterRender for code that must run after every change detection cycle (DOM measurements, scroll synchronization).
Third-party library pitfall: Libraries like Chart.js, D3, or Google Maps that access window or document at import time will crash the server even inside an afterNextRender block, because the import statement at the top of the file executes during module loading. The fix: use dynamic import() inside afterNextRender so the library code only loads in the browser.
import { Component, afterNextRender, afterRender } from '@angular/core';@Component({ selector: 'app-chart', template: `<div #chartContainer></div>`})export class ChartComponent { @ViewChild('chartContainer') container!: ElementRef; constructor() { // Runs ONCE after first render in browser afterNextRender(() => { // Safe to use browser APIs here this.initChart(); }); // Runs EVERY render in browser afterRender(() => { // Update chart on each render this.updateChart(); }); } private async initChart() { // Dynamic import ensures Chart.js is ONLY downloaded in the browser. // If you used a static import at the top of the file, the server would // try to load Chart.js during SSR and crash on "window is not defined". const { Chart } = await import('chart.js'); new Chart(this.container.nativeElement, {...}); }}
Share data between server and client to avoid duplicate requests. Without transfer state, here is what happens: the server fetches data from your API, renders the HTML, and sends it to the browser. Then the browser bootstraps Angular, which runs ngOnInit again and fetches the same data from the same API a second time — wasting bandwidth and causing a visible content flicker. Transfer state solves this by embedding the server-fetched data directly in the HTML payload, so the client can reuse it instead of re-fetching.
// data.service.tsimport { Injectable, inject, PLATFORM_ID } from '@angular/core';import { HttpClient } from '@angular/common/http';import { TransferState, makeStateKey } from '@angular/platform-browser';import { isPlatformServer } from '@angular/common';import { of, tap } from 'rxjs';const USERS_KEY = makeStateKey<User[]>('users');@Injectable({ providedIn: 'root' })export class DataService { private http = inject(HttpClient); private transferState = inject(TransferState); private platformId = inject(PLATFORM_ID); getUsers(): Observable<User[]> { // Check if data was transferred from server if (this.transferState.hasKey(USERS_KEY)) { const users = this.transferState.get(USERS_KEY, []); this.transferState.remove(USERS_KEY); // Clean up return of(users); } // Fetch from API return this.http.get<User[]>('/api/users').pipe( tap(users => { // Save to transfer state on server if (isPlatformServer(this.platformId)) { this.transferState.set(USERS_KEY, users); } }) ); }}
Generate static HTML at build time. Prerendering (also called Static Site Generation or SSG) is the best of both worlds for content that does not change on every request — you get the SEO benefits of SSR and the deployment simplicity of static files. The HTML is generated once during ng build, not on every request, so there is zero server load at runtime. Perfect for blog posts, documentation, marketing pages, and product pages that update infrequently.
SSR vs SSG decision: If the page content is the same for every visitor (a blog post, a product page), use prerendering — it is faster and cheaper. If the page content depends on the current user (a personalized dashboard, a user profile), you need runtime SSR because the server must render different HTML for each request.
Q: A stakeholder asks you to add SSR to an existing Angular SPA. How do you evaluate whether SSR is worth the complexity, and what commonly breaks?
Strong Answer: I evaluate SSR on three criteria. First, SEO — if the app is a marketing site, e-commerce catalog, or blog where search ranking matters, SSR is almost mandatory. Second, First Contentful Paint on slow networks — SSR cuts perceived load time from 5+ seconds to under 1 second for users on 3G. Third, social media previews — without SSR, sharing a link on LinkedIn shows a blank card.If the app is an internal dashboard behind authentication, SSR provides zero SEO benefit. The added complexity (server infrastructure, platform checks, hydration bugs) is not justified.What commonly breaks: any code that accesses window, document, localStorage, or navigator at import time or during rendering. Third-party charting libraries that assume a browser. WebSocket connections attempted during server render. The fix: wrap all browser API access with isPlatformBrowser checks and move DOM-dependent code into afterNextRender callbacks.Follow-up: How does hydration prevent the “flash” that older SSR approaches had?
Answer: With provideClientHydration(), Angular reuses the server-rendered DOM nodes instead of tearing them down and re-rendering. It attaches event listeners to existing elements without recreating them. Event replay (withEventReplay) captures user clicks that happen before hydration completes and replays them afterward, so no interaction is lost.
Q: Explain Transfer State. Why does Angular fetch data twice without it, and how does the automatic HTTP transfer cache work?
Strong Answer: Without transfer state, the server fetches data, renders HTML, and sends it. The browser then bootstraps Angular and fetches the same data again because the client component does not know it was already fetched. The user sees a flash where server-rendered content disappears during re-fetching.Transfer state embeds the server’s API responses as serialized JSON in the HTML. The client HttpClient checks this cache before making network calls. The automatic approach (withHttpTransferCacheOptions in provideClientHydration) handles this transparently for all GET requests. The includePostRequests option extends it to POST requests.The manual approach (TransferState service with makeStateKey) is needed for data fetched outside HttpClient — from third-party SDKs, WebSockets, or GraphQL clients.Follow-up: When would the automatic transfer cache cause problems?
Answer: When the API response depends on context that differs between server and client — for example, geolocation-based content where the server IP resolves to a different region. Also, large payloads: if the server fetches 10MB of data, that gets embedded in the HTML, making the initial download much larger.
Q: Compare SSR, SSG (prerendering), and ISR. When would you choose each for an Angular app?
Strong Answer: SSR renders every page on every request — best for frequently changing or user-specific content. SSG renders at build time into static HTML files — best for static content like docs or marketing pages, deployable to any CDN. ISR is a hybrid where pages are pre-rendered but regenerated in the background after a configurable interval.My decision framework: content changes less than daily, use SSG. Changes frequently but is not user-specific, use ISR or SSR with caching. User-specific or real-time, use full SSR. For e-commerce: product listing pages get SSG/ISR, product detail pages get SSR (price must be current), cart is client-side only.Follow-up: Can you mix these strategies in one Angular app?
Answer: Yes. In angular.json, the prerender config specifies which routes to prerender as static HTML. Remaining routes fall through to the SSR server. Client-only routes behind auth guards skip both. You can prerender /about, SSR /products/*, and client-render /dashboard in the same deployment.