Skip to content

Angular SSR Explained — Server-Side Rendering with Angular Universal

DodaTech Updated 2026-06-28 7 min read

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:

  1. Adds @angular/ssr package
  2. Creates server.ts for the Express server
  3. Adds SSR configuration to angular.json
  4. Modifies main.ts to 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

  1. Not handling browser-only APIs — Accessing window, document, or localStorage without checking isPlatformBrowser causes errors during SSR.

  2. Large initial bundle with SSR — SSR does not reduce JavaScript bundle size. Users still download the full app. Optimize with lazy loading.

  3. 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.

  4. Over-caching SSR responses — Caching too aggressively may serve stale content. Use appropriate TTLs and cache invalidation.

  5. Not preloading critical resources — SSR HTML should include preload hints (<link rel="preload">) for critical CSS and lazy chunks.

Practice Questions

  1. What is the main benefit of SSR? Faster initial page loads (FCP/LCP) and SEO for search engine crawlers that do not execute JavaScript.

  2. What is hydration? The process where Angular attaches event listeners to server-rendered DOM nodes without re-rendering them.

  3. How do you check if code runs on the server? Use isPlatformServer(PLATFORM_ID) from @angular/common.

  4. What is Angular Universal? The previous name for Angular's SSR solution, now integrated as @angular/ssr.

  5. 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

Does SSR work with all Angular features?

Most features work, but avoid direct DOM manipulation. Use Angular's Renderer2 or platform checks.

How do I deploy an SSR app?

Build with ng build, which produces both browser and server bundles. Deploy the server bundle to Node.js hosting (Vercel, Firebase, AWS, etc.).

Can I pre-render static pages instead of live SSR?

Yes, use ng build with prerendering for static pages. Dynamic pages use live SSR.

What is transfer state?

A mechanism to transfer server-fetched data to the client via a script tag, preventing re-fetching on hydration.

Does SSR affect Time to Interactive (TTI)?

SSR improves FCP but may delay TTI slightly because JavaScript still needs to load and hydrate.

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