post's image

How Angular SSR Works Under the Hood

Ghost wrote 9 hours ago (Aug 2, 2025) with 10👁️ | 8 mins read

Angular Server-Side Rendering (SSR), often referred to as Angular Universal, is a powerful technique that significantly enhances the performance and SEO of your Angular applications. While it offers impressive benefits, understanding its inner workings can sometimes feel like peering into a black box. This post will demystify Angular SSR, explaining the core processes, key concepts, and how your code behaves differently on the server versus the client.

Why Angular SSR? The Problem It Solves

Before diving into "how," let's quickly recap "why." Traditional Single Page Applications (SPAs) rendered purely on the client-side face two main challenges:

  1. Poor SEO: Search engine crawlers primarily see an empty HTML shell initially. While modern crawlers are better at executing JavaScript, relying solely on client-side rendering can still negatively impact indexing and ranking.
  2. Slow Initial Load (Flickering Content): Users might experience a blank screen or a loading spinner while the JavaScript bundles download and the application bootstraps in the browser. This leads to a higher First Contentful Paint (FCP) and a less satisfying user experience.

Angular SSR addresses these by generating the initial HTML on the server, sending a fully rendered page to the browser.


The Two Phases of Angular SSR

An Angular SSR application's lifecycle can be broken down into two distinct, yet interconnected, phases:

1. Server-Side Rendering (SSR) Phase

This phase occurs entirely on your server (typically a Node.js environment).

What Happens:

  1. Initial Request: A user's browser sends an HTTP request (e.g., yourdomain.com/blog/my-article) to your server.
  2. Angular Application Bootstrapping (on Server): Instead of serving a static HTML file, your server runs an instance of your Angular application in a Node.js environment. This is possible because Angular Universal provides a server-specific platform (@angular/platform-server) that mimics a browser environment sufficiently for Angular to render.
  3. Data Fetching: The Angular application on the server identifies what data it needs to display the requested route. It then makes API calls to your backend services to fetch this data. For instance, if it's a blog post, it would call an endpoint like /api/posts/my-article.
  4. HTML Generation: Once the data is retrieved, the Angular application renders all the necessary components for that specific route into a complete HTML string. This is essentially what you'd see if you right-clicked "View Page Source" on a fully loaded page.
  5. State Transfer (Serialization): This is a vital step. The data that Angular fetched from your backend during the server-side rendering is serialized (e.g., converted to JSON) and embedded directly into the generated HTML, typically within a <script> tag. This "pre-filled" data is known as the TransferState.
  6. Response Sent: The server then sends this generated HTML (along with the embedded TransferState and references to your Angular JavaScript bundles) back to the user's browser.

Key Characteristics of the SSR Phase:

  • No Browser APIs: During this phase, Angular code does not have access to browser-specific APIs like window, document, localStorage, etc. If your code tries to access these, it will throw errors. You must guard against this using isPlatformBrowser() checks.
  • Synchronous-like Rendering: While data fetching is asynchronous, the actual component rendering on the server is a single pass to generate the full HTML before sending it.

2. Hydration Phase (Client-Side Rendering)

This phase occurs in the user's browser after the initial HTML is received.

What Happens:

  1. Initial Display: The browser receives the fully rendered HTML and immediately displays it to the user. This results in a very fast First Contentful Paint (FCP), as the user sees meaningful content instantly, without waiting for JavaScript.
  2. JavaScript Download & Bootstrapping: In the background, the browser downloads and parses your Angular application's JavaScript bundles.
  3. Application Re-bootstrapping (on Client): Once the JavaScript is loaded, Angular re-boots your application in the browser environment.
  4. Hydration (New in Angular 17+):
    • Prior to Angular 17, the application would essentially re-render the entire component tree on the client, which could lead to a slight "flicker" or performance hit.
    • With Hydration, Angular intelligently "attaches" its internal data structures and event listeners to the existing DOM nodes that were generated by the server. It doesn't re-render the DOM; it "activates" it. This makes the transition from server-rendered content to interactive application seamless.
  5. State Rehydration: Angular reads the TransferState that was embedded in the HTML by the server. This means that if a component needed data that was already fetched on the server, it will not make another API call. It simply uses the data it received from the TransferState.
  6. Full Interactivity: Once hydration is complete, your Angular application is fully interactive, just like a regular client-side rendered SPA. Subsequent navigation or data fetching will occur entirely on the client-side via traditional API calls to your public backend endpoints.

Key Characteristics of the Hydration Phase:

  • Browser APIs Available: Full access to window, document, localStorage, etc.
  • Interactive: The application becomes responsive to user input.
  • Optimized Data Fetching: Avoids redundant API calls for initial data due to State Transfer.

Key Concepts and Tools

  • @angular/platform-server: The package that provides the necessary tools and environment for Angular to run on a Node.js server.

  • PLATFORM_ID, isPlatformServer(), isPlatformBrowser(): These utilities from @angular/core and @angular/common allow you to conditionally execute code based on whether the application is running on the server or in the browser. This is crucial for handling browser-specific APIs or differentiating API endpoints.

    import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
    import { isPlatformServer, isPlatformBrowser } from '@angular/common';
    
    @Injectable({ providedIn: 'root' })
    export class MyService {
      constructor(@Inject(PLATFORM_ID) private platformId: Object) {
        if (isPlatformServer(this.platformId)) {
          console.log('Running on server');
          // Use localhost for API calls
        } else if (isPlatformBrowser(this.platformId)) {
          console.log('Running in browser');
          // Use public API URL
        }
      }
    }
    
  • TransferState (@angular/platform-browser): The mechanism for passing data fetched on the server to the client-side to prevent re-fetching. You use makeStateKey to create unique keys for your data.

    import { makeStateKey, TransferState } from '@angular/platform-browser';
    
    const MY_DATA_KEY = makeStateKey<any>('myData');
    
    // In your service/component on the server:
    this.http.get('http://localhost:3000/api/data').subscribe(data => {
      this.transferState.set(MY_DATA_KEY, data);
    });
    
    // In your service/component on the client:
    if (this.transferState.has(MY_DATA_KEY)) {
      const data = this.transferState.get(MY_DATA_KEY, null);
      this.transferState.remove(MY_DATA_KEY); // Clean up state
      // Use the data
    } else {
      // Fetch data normally if not present (e.g., subsequent navigation)
    }
    
  • ng add @angular/ssr: The Angular CLI command that simplifies the setup process for Angular Universal by adding the necessary dependencies and boilerplate code.


The Request Flow in Summary

  1. User Request: Browser sends request to yourdomain.com.
  2. Nginx/Load Balancer: Forwards request to your EC2 instance.
  3. EC2 Instance:
    • Angular SSR (Node.js Process):
      • Receives request.
      • Makes internal API call to http://localhost:<BACKEND_PORT> (your Node.js backend process).
      • Receives data from backend.
      • Renders HTML and embeds TransferState.
      • Sends HTML response to browser.
    • Node.js Backend (separate process):
      • Receives internal API call from Angular SSR.
      • Processes data and sends response back to Angular SSR.
  4. User Browser:
    • Displays initial HTML immediately.
    • Downloads Angular JS bundles.
    • Hydrates the application using the received HTML and TransferState (avoiding redundant API calls).
    • Application becomes fully interactive. Subsequent API calls go directly to yourdomain.com/api (which Nginx then proxies to localhost:<BACKEND_PORT>).

Conclusion

Angular SSR, with its two-phase rendering and intelligent hydration, provides a robust solution for building performant and SEO-friendly web applications. By understanding how the server and client environments interact, and leveraging tools like PLATFORM_ID and TransferState, you can build more efficient and resilient Angular Universal applications. This deep dive should equip you with the knowledge to troubleshoot common SSR issues and optimize your application's initial load experience.