Server Side Rendering with Angular Universal

Seth Gwartney

Single-page Apps (SPAs) are aptly named - there is literally only one single HTML document that is served initially to a client. Any new views that are required in the app are generated solely on the client via JavaScript. The request-response cycle still happens, but this is usually only to RESTful APIs for data; or to get static resources, like images.

There are a lot of benefits to writing applications this way. However, there are some things that you lose, such as the ability for web crawlers to traverse your app and slower performance while the app is loading, which can take a significant amount of time. In comes Server-Side Rendering (SSR) to bridge the gap.

SSR with Angular Universal

An Angular application is a Single-page App - it runs in a client’s browser. Angular Universal, however, let’s you also run your Angular app on the server. This enables you serve static HTML to the client. With Angular Universal, the server will pre-render pages and show your users something, while the client-side app loads in the background. Then, once everything is ready client-side, it will seamlessly switch from showing the server-rendered pages to the client-side app. Your users shouldn’t notice a difference, beyond the fact that instead of waiting for your “Loading” spinner to finish, they at least can have some content to keep them engaged until they can start using the fully-featured client-side application.

SSR with Angular Universal requires changes in both the client application and the server stack to work. For this article, we’ll assume this is a brand new Angular application, barely created with the Angular CLI, at version >= 7. Just about any server technology can run a Universal app, but it has to be able to call a special function, renderModuleFactory(), provided by Angular Universal, which is itself a Node package; so serving this Angular application from an Node/Express server makes sense for this example.

Adding Universal to Your App

We’ll be using a schematic to get us up and going quickly. From your app directory open a terminal and run the following command:

$ ng add @nguniversal/express-engine --clientProject {{ name of your project }}

You’ll notice this schematic made several changes to your app, modifying some files and adding some files. I’ll briefly go through these in no particular order.

  1. angular.json: projects.{{project-name}} changes to “dist/browser”; and a new projects.{{project-name}}.architect is added, called “server”. This lets the Angular CLI know about our server/Universal version of the Angular application.
  2. package.json: besides adding a few new dependencies (to be expected), we also get a few new scripts: “compile:server”, “server:ssr”, “build:ssr”, “build:client-and-server-bundles”.
  3. server.ts: this is the NodeJS Express server. Obviously, you don’t have to use this exact server setup as generated, although make note of the line app.engine('html', ngExpressEngine({...'. ngExpressEngine is a wrapper around renderModuleFactory which is the special sauce that makes Universal work. If you don’t use this exact server.ts file for your set-up, at least copy this part out and integrate it into yours.
  4. webpack.server.config.js: the webpack configuration for bundling the Express/Universal server. Again, if you are integrating Universal in an existing app, you might be doing something different. However, since we’ve added our Universal module to the server, which has the whole dependency graph of Angular, this makes a tidy bundle of everything that’s needed for a production build. The configuration is used in conjunction with the new package.json script “compile:server”.
  5. main.ts: this has been modified so that the browser version of the app won’t start bootstrapping until the Universal-rendered pages have been fully loaded.
  6. main.server.ts: this new file basically only exports the AppServerModule, which is the entry point of the Universal version of the application. We’ll see this soon.
  7. tsconfig.server.json: this tells the Angular compiler where to find the entry module for the Universal application.
  8. app.module.ts: modified to execute the static method .withServerTransition on the imported BrowserModule. This tells the browser version of the application that the client will be transitioning in from the server version at some point.
  9. app.server.module.ts: this is the root module for the server version only. You can see it imports our AppModule, as well as the ServerModule from @angular/platform-server, and bootstraps the same AppComponent as AppModule. AppServerModule is the entry point of the Universal application.

Start Your Universal App

From a command line, run the following command:

$ npm run build:ssr && npm run serve:ssr

Assuming you didn’t hit any snags during the build process, open your browser to http://localhost:4000 (or whatever port is configured for you), and you should see your Universal app in action! It won’t look any different, but the first page should load much quicker than your regular Angular application. If your app is small and simple, this might be hard to notice. You can try throttling the network speed by opening Chrome Dev Tools, and under the Network tab, finding the dropdown that says “Online”. Select “Slow 3G” to mimic a device on a slow network - you should still see good performance for your landing page, and any routed pages you go to.

Also, try viewing the page source (right-click on page and select “View Page Source”). You’ll see all normal HTML in the <body> tag that matches what is displayed on your page - meaning, your application can be meaningfully scraped by a web crawler. Compare this with the page source of a non-Universal application, and all you’ll see in the <body> tag is <app-root> (or whatever you’ve called the selector for your bootstrapped AppComponent).

Things to Note

Since a Universal application runs on the server and not in a browser, there are a few things you need to watch out for in your application code:

  • Check your use of browser-specific objects, such as window, document, or location. These don’t exist on the server. You shouldn’t be using these anyway; try using an injectable Angular abstraction, such as Document or Location. As a last resort, if you do truly need them, wrap their usage in a conditional statement, so that they’ll only be used by Angular on the browser. You can do this by importing the functions isPlatformBrowser and isPlatformServer from @angular/common, injecting the PLATFORM_ID token into your component, and running the imported functions to see whether you’re on the server or the browser.
  • If you use ElementRef to get a handle on an HTML element, don’t use the nativeElement to manipulate attributes on the element. Instead, inject Renderer2 and use one of the methods there.
  • Browser event handling won’t work. Your app won’t respond to click events or other browser events when running on the server. However, any link generated from a routerLink will work for navigation.
  • Avoid the use of setTimeout, where possible.
  • Make all URLs for server requests absolute. Requests for data from relative URLs will fail when running from the server, even if the server can handle relative URLs.
  • Similarly, security around HTTP requests issued from a server isn’t the same as those issued from a browser. Server requests may have different security requirements and features. You’ll have to handle security on these requests yourself.

Further Reading

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#