Angular SSR Explained — Server-Side Rendering with Angular Universal
In this tutorial, you will learn about Angular SSR Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
Angular server-side rendering (SSR) renders Angular applications on the server, sending fully rendered HTML to the client for faster initial page loads and better SEO.
What You'll Learn
- What SSR is and why it matters
- How to add SSR to an existing Angular project
- How hydration bridges server and client rendering
- How to handle browser-only APIs in SSR
- How to optimize SSR performance
Why It Matters
SSR improves two critical metrics: First Contentful Paint (FCP) and Largest Contentful Paint (LCP). It also ensures search engines can crawl your content, which is essential for public-facing Angular applications.
Real-World Use
Durga Antivirus Pro's public-facing threat report pages use SSR to ensure they appear in search results with proper descriptions and render instantly for users who click through from search.
flowchart LR
A[Browser Request] --> B[Server]
B --> C[Angular Universal]
C --> D[Render HTML]
D --> E[Send HTML]
E --> F[Browser Displays HTML]
F --> G[Hydration]
G --> H[Interactive SPA]
style A fill:#f97316,color:#fff
Adding SSR to a Project
Use the Angular CLI to add SSR:
ng add @angular/ssr
This command:
- Adds
@angular/ssrpackage - Creates
server.tsfor the Express server - Adds SSR configuration to
angular.json - Modifies
main.tsto export the app config
After installation:
// server.ts - auto-generated
import "zone.js/dist/zone-node";
import { ngExpressEngine } from "@angular/ssr";
import express from "express";
import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
import bootstrap from "./src/main.server";
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, "../browser");
const indexHtml = join(serverDistFolder, "index.server.html");
server.engine("html", ngExpressEngine({ bootstrap }));
server.set("view engine", "html");
server.set("views", browserDistFolder);
server.get("*", (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: "REQUEST", useValue: req }] });
});
return server;
}
Expected output: The app now serves fully rendered HTML on first request. JavaScript loads in the background for subsequent navigation.
The Express server uses Angular Universal's ngExpressEngine to render pages on the server. For each request, it renders the requested route and returns the HTML.
Hydration
Hydration attaches event listeners to the server-rendered DOM:
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { provideClientHydration } from "@angular/platform-browser";
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(),
]
});
Expected output: The browser downloads the server-rendered HTML, displays it immediately, then loads JavaScript and attaches event handlers without re-rendering the DOM.
Without hydration, Angular would replace the server-rendered DOM entirely, causing a flash. With hydration, Angular reuses the existing DOM nodes and attaches component logic.
Handling Browser-Only APIs
Some APIs are only available in the browser. Guard against server calls:
import { Injectable, PLATFORM_ID, Inject } from "@angular/core";
import { isPlatformBrowser, isPlatformServer } from "@angular/common";
@Injectable({ providedIn: "root" })
export class PlatformService {
constructor(@Inject(PLATFORM_ID) private platformId: object) {}
get isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
get isServer(): boolean {
return isPlatformServer(this.platformId);
}
getLocalStorage(key: string): string | null {
if (this.isBrowser) {
return localStorage.getItem(key);
}
return null;
}
getDocument(): Document | null {
if (this.isBrowser) {
return document;
}
return null;
}
}
Usage in component:
@Component({
selector: "app-scroll-tracker",
standalone: true,
template: `<p>Scroll position: {{ scrollY }}px</p>`
})
export class ScrollTrackerComponent implements OnInit, OnDestroy {
scrollY = 0;
private listener = () => {};
constructor(public platform: PlatformService) {}
ngOnInit() {
if (this.platform.isBrowser) {
this.listener = () => {
this.scrollY = window.scrollY;
};
window.addEventListener("scroll", this.listener);
}
}
ngOnDestroy() {
if (this.platform.isBrowser) {
window.removeEventListener("scroll", this.listener);
}
}
}
Expected output: On the server, the component renders without scroll tracking. In the browser, it tracks and displays scroll position.
Always check isPlatformBrowser before accessing window, document, localStorage, or any browser-only API. The server will throw errors if these are accessed during rendering.
SEO with SSR
SSR enables proper meta tags for SEO:
import { Component, OnInit } from "@angular/core";
import { Meta, Title } from "@angular/platform-browser";
@Component({
selector: "app-product-page",
standalone: true,
template: `<h1>{{ product.name }}</h1>`
})
export class ProductPageComponent implements OnInit {
product = { name: "Premium Widget", description: "A high-quality widget for all your needs.", price: 29.99 };
constructor(private meta: Meta, private title: Title) {}
ngOnInit() {
this.title.setTitle(`${this.product.name} - Buy Now | DodaTech Store`);
this.meta.updateTag({ name: "description", content: this.product.description });
this.meta.updateTag({ property: "og:title", content: this.product.name });
this.meta.updateTag({ property: "og:description", content: this.product.description });
this.meta.updateTag({ property: "og:type", content: "product" });
}
}
Expected output: Search engines see the complete HTML with meta tags, product name, and description when crawling the page.
SSR ensures meta tags are present in the initial HTML response. Without SSR, Angular renders meta tags client-side, and search engines may not execute JavaScript before indexing.
SSR Performance Optimization
Optimize SSR with caching and selective rendering:
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable, of } from "rxjs";
import { shareReplay, tap } from "rxjs/operators";
@Injectable({ providedIn: "root" })
export class SsrCacheService {
private cache = new Map<string, { data: any; expires: number }>();
private defaultTTL = 60000; // 1 minute
getOrFetch<T>(key: string, fetchFn: () => Observable<T>, ttl = this.defaultTTL): Observable<T> {
const cached = this.cache.get(key);
if (cached && Date.now() < cached.expires) {
return of(cached.data as T);
}
return fetchFn().pipe(
tap(data => {
this.cache.set(key, { data, expires: Date.now() + ttl });
})
);
}
clear() {
this.cache.clear();
}
}
Expected output: Frequently accessed data is cached in memory, reducing server load and improving SSR response times.
SSR caching is critical for production. Without it, every page request triggers full rendering on the server. Cache at the HTTP level (CDN) and at the application level (in-memory cache).
Common Mistakes
Not handling browser-only APIs — Accessing
window,document, orlocalStoragewithout checkingisPlatformBrowsercauses errors during SSR.Large initial bundle with SSR — SSR does not reduce JavaScript bundle size. Users still download the full app. Optimize with lazy loading.
Ignoring hydration errors — If the server-rendered HTML differs from the client-rendered version, hydration fails and Angular re-renders. Use
provideClientHydration()with consistent rendering.Over-caching SSR responses — Caching too aggressively may serve stale content. Use appropriate TTLs and cache invalidation.
Not preloading critical resources — SSR HTML should include preload hints (
<link rel="preload">) for critical CSS and lazy chunks.
Practice Questions
What is the main benefit of SSR? Faster initial page loads (FCP/LCP) and SEO for search engine crawlers that do not execute JavaScript.
What is hydration? The process where Angular attaches event listeners to server-rendered DOM nodes without re-rendering them.
How do you check if code runs on the server? Use
isPlatformServer(PLATFORM_ID)from@angular/common.What is Angular Universal? The previous name for Angular's SSR solution, now integrated as
@angular/ssr.How does SSR affect bundle size? SSR does not reduce bundle size. Users still download the full client app after the initial HTML renders.
Challenge
Build an SSR-optimized BlogPageComponent that renders blog posts. The server renders the full post HTML with meta tags (title, description, og:image). The client hydrates and adds interactivity (comments form, share buttons, reading progress). Handle the case where the blog data comes from an API — cache the response on the server for 5 minutes. Add loading placeholders for comments (loaded client-side after hydration).
FAQ
Mini Project
Convert an existing Angular app to use SSR. Add SSR with ng add @angular/ssr. Create a product listing page that renders on the server with complete product cards. Add a PlatformService that safely handles browser-only APIs (local storage for theme, window resize). Configure hydration and verify the app renders correctly both server-side and client-side. Add caching for the product API on the server side.
What's Next
Continue with testing and Internationalization:
Angular Testing, Angular i18n, Angular Standalone
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro