Takeaways from running Angular Universal in production

Jacob Overgaard
Jul 31, 2020 · 5 min read

Toying around with Angular Universal (server-side rendering or SSR for short) with the latest projects at IMPACT-Developers taught us a lot of things. This article focuses mainly on Angular 8+, and has been tested with versions 8 through 10, but the techniques used should work for other versions as well.

Angular Universal logo
Angular Universal

First things first: Why Angular Universal?

All right, so you have come this far and figured out that you might benefit from Angular Universal, or maybe you are trying to figure out the same. In Angulars own words you should use SSR for three reasons:

1. Facilitate web crawlers through search engine optimization (SEO)

2. Improve performance on mobile and low-powered devices

3. Show the first page quickly with a first-contentful paint (FCP)

Another point, that we have learned, is that bootstrapping your app through the server allows you to share server variables with your app. This might be credentials, like the Application Insights key set as an environment variable in an Azure Web App, which you then do not have to commit to your code repository.

Getting started with Angular Universal

The Angular team has published a pretty extensive guide on how to install Angular Universal. This was something not available in the early days of Angular Universal, when it was still a 3rd party tool. I am very pleased, that the Angular team is making an extraordinary effort to support Angular Universal.

My article will lean itself up against that guide and provide information in addition to the guide in relation to real world use cases.

From this point on, I will assume that you have a working project with the base Angular Universal up and running. Make sure to test the full build of your project before proceeding, because by default Angular Universal will build a production bundle.

Hint: From Angular 9 you get a script in package.json called “dev:ssr”, which is eminent to test your build

Let us take a breather and talk about the API/backend

I want to make a special mention of the API/backend, while your Angular app of course has one of those, but what happens in an SSR build is that Node.js is used to render the app, and that also means that the built-in HttpModule is running on Node.

Hint: If you have a fully qualified URL to your backend (with https:// and all that), then you do not have to change anything and may move on.

If you, like me, have a relative URL to your api (/api for instance) you will soon discover, that none of your API calls are working.

Many headaches were had over this problem, but it turns out to be a simple fix, and Angular has also introduced a solution in the aforementioned guide on Universal: You need to provide the http origin to the HttpModule, but only when running on the server before the browser takes over.

Simple as that you create an interceptor which intercepts all http requests and prepends the server URL to the URL property.

Universal Interceptor setup (author: Google)

Notice how the interceptor is only provided in the AppServerModule?

All done, you should now have a production grade setup, that proxies backend requests through /api and makes a fast first-contentful paint.

Preserve state from server to browser

Moving back to the browser again. The TransferHttpCacheModule utilises Angular’s BrowserTransferStateModule and that module provides a service called TransferState, which you use to rehydrate the application once the browser module is bootstrapped.

Tip: Caching HTTP requests

The Angular team has seen fit to provide a small interceptor, that caches all http requests made on the server and uses that cache to rehydrate the application again once the browser takes over, so the browser does not have to make all the same requests over again.

import { TransferHttpCacheModule } from '@nguniversal/common';@NgModule({
imports: [
BrowserModule.withServerTransition({ appId: 'my-app' }),
BrowserTransferStateModule,
TransferHttpCacheModule
]
})
export class AppModule {}

Simply import the module on the browser module (and remember to include the BrowserModule andBrowserTransferStateModule on the browser bundle as well as the ServerTransferStateModule on the server module.

Angular will now cache all http requests on the server with the full URL as cache key and rehydrate again on the browser.

Tip: Cache just about everything

You can use this service to transfer just all sorts of stuff: lists of data, flags, and other cached resources from the server that was provided in server.ts.

See example here for a list:

Tip: Lifecycles

We quickly learned that all components run their lifecycle again in the browser module, so make sure to consider what should be transferred from the server, and also please remember to use trackBy in your *ngFor loops, so elements are not rendered again.

Tip: isPlatformBrowser and Window

Check the Angular example from before on StackBlitz to see how we use isPlatformServer to only perform certain logic on the server module. There is also the equivalent isPlatformBrowser that you can use to run logic only on the browser. This is especially useful when having to use the Window object, which does not exist on the server in a Node.js context:

I cannot stress this point enough: Check all usages for window and encapsulate them in isPlatformBrowser !

Conclusion

Angular Universal is a great tool, but it has a lot of pitfalls, especially considering that you are responsible for thinking “what is running on the server” and “what is running on the browser”.

On a side note I found, that hosting an Angular Universal app in Azure requires quite a big server for a moderately visited site. In our case we had to run our web app (Docker container) on a P2v2 App Service Plan for the CPU and memory to stay stable.

We now serve a full HTML response to each visitor, which is SEO friendly and fast.

Is it worth the trouble? Yes!

IMPACT Developers

Leading digital agency in Denmark