An Introductory Guide to Angular SSR

Blog post image for An Introductory Guide to Angular SSR
Michael Cassar's profile pictureMichael Cassar

Greetings mortals! As I sit in bed, sipping a glass of wine, contemplating my life choices, I've come to realise that the very website I am building, the one you are on, and the technology that I'm building it with, would make a very good first blog post.

I've been using Angular since 2015 in projects of varying complexity, but a feature of Angular that I've never had the opportunity to explore is Angular Universal aka Angular Server Side Rendering or Angular SSR.

What is Angular SSR?

Angular SSR does exactly what it says on the tin. Typically, an Angular application ends up being built into various bundles of JavaScript files, an HTML file, CSS files, and assets, which are served to a client browser. The browser then executes these JavaScript files, handling the rendering of your views, logic, and loading your data, typically done via HTTP requests. On the other hand, SSR delivers a pre-rendered HTML file with all the required assets to the client browser.

One could argue that your application would work perfectly fine as it is, and you'd be right. So, why would you need to enable Angular SSR on your Angular application? There are a couple of reasons:

  • Improved Data Security and Compliance - Angular SSR significantly enhances data security and compliance measures. By pre-rendering content on the server, sensitive data is shielded from client-side exposure, fortifying the application against potential security vulnerabilities and ensuring adherence to compliance standards.
  • Faster Load Times - Angular SSR stands as a catalyst for an optimized web experience, prominently reflected in faster load times. The server-side pre-rendering approach expedites content delivery to the user's browser, culminating in reduced loading periods and a highly responsive user interface.
  • Less client-side Processing - Angular SSR streamlines web applications by offloading rendering responsibilities to the server, minimizing the demand for extensive client-side processing. This optimization contributes to a smoother user experience, particularly on devices with constrained processing capabilities.
  • Facilitates SEO - Beyond its performance enhancements, Angular SSR emerges as a proficient tool for Search Engine Optimization or SEO. The delivery of pre-rendered HTML files to the client facilitates seamless crawling and indexing by search engines. This strategic approach enhances the visibility and ranking of your web application in the competitive digital landscape.

Enabling Angular SSR

To bootstrap your Angular SSR journey, simply run one of the following commands. If this is your first time exploring this feature, I highly recommend trying it in a test application first. Implementing it in an existing application may require numerous code changes, so it's beneficial to familiarize yourself with the process in a controlled environment first.

# Generate a new Angular SSR application
ng new --ssr

# Add Angular SSR to an existing Angular application
ng add @angular/ssr

After running these commands, you'll notice some new files in your Angular project structure. To briefly explain the server.ts file, since it's pretty much the only important one; it's basically a NodeJS Express application responsible for server-side rendering.

Feel free to customize the server.ts file as you wish. For instance, you can add security headers to your application. Seriously, don't be naughty; make sure to add these!

Last but not least, you'll need to say goodbye to your trusty ng serve command, instead you'll need to substitute it with npm run dev:ssr.

Platforms

Angular SSR executes your Angular application components on both the server and the client. When a client requests a route, the NodeJS application executes the route on the server, runs any server-compatible code, and sends the pre-rendered HTML response to the client. Therefore, it's crucial to verify whether your code is currently running on the client or on the server or use built in Angular injectors. Some client-side APIs, won't be accessible on the server, leading to rendering failures.

The example below shows how to achieve this. The resulting boolean values, isBrowser and isServer, can then be utilized in if statements across your code. Creating a service, instead of duplicating this code throughout your application, is also a good practice.

import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';

@Component({
  selector: 'app-weather-report',
  templateUrl: './weather-report.component.html',
  styleUrl: './weather-report.component.scss'
})
export class WeatherReportComponent {

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {

    this.isBrowser = isPlatformBrowser(platformId);
    this.isServer = isPlatformServer(platformId);
  }
}

Data Transfer

Now that we've delved into the basics of SSR, we can begin pre-rendering content. To achieve this, we need to create a service that will manage our state. The reason for handling state is that, despite pre-rendering content on the server, when the Angular application executes in a client browser, all the state within your JavaScript will be lost since your code is executing in a different context. This code essentially serves as a wrapper around Angular's TransferState class, which is a key-value store transferred from the server to the client.

import { Injectable, TransferState, makeStateKey } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StateService {

  constructor(private transferState: TransferState) { }

  set<T>(key: string, data: T): void {

    this.transferState.set<T>(makeStateKey(key), data);
  }
  
  get<T>(key: string, defaultValue: any = undefined): T {

    const state = this.transferState.get<T>(makeStateKey(key), defaultValue);

    this.transferState.remove(makeStateKey(key));

    return state;
  }
  
  hasKey<T>(key: string) {

    return this.transferState.hasKey<T>(makeStateKey(key));
  }
}

Imagine that we need to load a weather report. In our Angular application, we would make an API call to our service to retrieve the weather report information. Subsequently, we'd store the result using our state service. Additionally, we will assign it to a property in our class so that the component can be pre-rendered. When our content is loaded on the client, we will then retrieve the weather report data from our state service. As I previously mentioned, I'm sipping wine. The following code is shown for demonstration purposes, and needs reworking.

async getWeatherReport(): Promise<any> {

  return firstValueFrom(this.httpClient.get(`https://localhost:1234/weather-report/${this.weatherReportId}`));
}

async ngOnInit(): Promise<void> {

  if (this.isServer) {

    var weatherReport = await this.getweatherReport();

    this.stateService.set(`weather-report-${this.weatherReportId}`, weatherReport);

    this.weatherReport = weatherReport;
  }
  else if (this.isBrowser) {

    this weatherReport = this.stateService.get(`weather-report-${this.weatherReportId}`);
  }
}

Router

Something I'd like to briefly touch on is that when using the Angular Router to link to various areas within your app, the application sometimes does not pre-render content for you, and you end up with a blank screen.

Essentially, what is happening is that the router is loading or re-using the route on the client and is not requesting the resource from the server, resulting in an empty state. To counteract this, ensure that you request the data from the server at client level when the state is not present.

SSR in Production

Angular SSR provides the ability to generate two types of builds, these are:

  • npm run build:ssr - This build option compiles your site into files compatible with a NodeJS Express application, as depicted in server.ts. Depending on the requested route, SSR will provide a pre-rendered HTML file tailored to that specific route for delivery to the browser. In essence, this process involves rendering on the server, known as server-side rendering. This build option requires the NodeJs Express application to be deployed on your environment.
  • npm run prerender - Executing the second build option, utilizes the server.ts to pre-render each route, generating individual index.html files. These files are organized in a directory structure corresponding to your routes, resulting in compiled code resembling a conventional static website. In essence, this process involves rendering before deployment, commonly referred to as pre-rendering. This build option does not require the NodeJs Express application to be deployed on your environment.

Conclusion

In summary, Angular Server-Side Rendering elevates your web application to superhero status. Boosting load times, tightening security, and simplifying processing, SSR is the secret sauce for a top-notch site.

Mastering Angular's TransferState ensures smooth state management. Picture this: content glides in seamlessly, data flows effortlessly.

Come showtime, choose your build wisely - SSR for instant magic or pre-rendering for a static website vibe. Angular SSR: where performance meets sophistication, making your web application the hero it deserves to be. Cheers to a faster, sleeker digital journey!