How to Implement Angular Universal (SSR/SSG)

seanbh

seanbh

Posted on August 28, 2023

How to Implement Angular Universal (SSR/SSG)


Photo by Guillermo Ferla on Unsplash

In my previous post we took a look at CSR, SSR, SSG and ISR in Angular. In this post, I want to walk through how to setup Angular Universal to achieve SSR/SSG.

The Starter App

This post is just about setting up Angular Universal, so we want to start with a completed application. We’ll use the standard Tour of Heroes app that you can download here. If you want to follow along, download that app now.

Once downloaded, run:

npm i
ng serve -o
Enter fullscreen mode Exit fullscreen mode

This will install dependencies and open a browser with the app running. Open the dev tools, go to the Network tab, and do two things to help us test:

  • Check ‘Disable cache’
  • Change ‘No Throttling’ to ‘Fast 3G’ to simulate a slower connection

Refresh the browser. In my test, it took ~20 seconds to load the app, during which time I was presented with a blank white screen. Note that this is the dev build which has not been optimized, and the production build will take significantly less time. But the increased time here helps to emphasize the point.


Screenshot of network tab

The dashboard document, which is just the index.html file with the scripts injected, loaded in less than a second. But that document doesn’t display anything until all of the JavaScript has been loaded. Let’s see what happens when we add Angular Universal.

Add Angular Universal

To add Angular Universal, stop the running app and execute:

ng add @nguniversal/express-engine
npm run dev:ssr
Enter fullscreen mode Exit fullscreen mode

Note that I usually use PowerShell as my terminal but when I execute npm run dev:ssr and make changes to the file, I intermittently receive an error, and I’ve read that other people have the same issue. So for this reason I use a bash shell to run this command.

Navigate away from the app and then back to it. What is different? Now the page loads in less than a second rather than 20 seconds. Look at the network tab. Again, the dashboard document loads in under a second but now a full page is being delivered rather than just a shell.

But the page is not interactive at that point. We still have to wait for the JavaScript to load for it to be interactive.

Adding Some Code to Help Visualize

Now let’s add some code to help us visualize what is going on.

Replace the contents of app.component.ts with this:

import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import {
  Component,
  PLATFORM_ID,
  TransferState,
  inject,
  makeStateKey,
} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'Tour of Heroes';

  platformId = inject(PLATFORM_ID);
  transferState = inject(TransferState);

  browserTime?: string;
  serverTime?: string;

  ngOnInit() {
    const serverTimeStateKey = makeStateKey<string>('serverTime');

    if (isPlatformBrowser(this.platformId)) {
      // set the browser time now and every second after
      this.setBrowserTime();
      setInterval(() => this.setBrowserTime(), 1000);

      // set the serverTime from transfer state
      this.serverTime = this.transferState.get(
        serverTimeStateKey,
        "I don't know, I wasn't generated on the server."
      );
    } else if (isPlatformServer(this.platformId)) {
      // set the serverTime and put in transfer state for the browser to read
      this.serverTime = new Date().toLocaleTimeString('en-US');
      this.transferState.set(serverTimeStateKey, this.serverTime);

      console.log('I am being rendered on the server');
    }
  }

  setBrowserTime() {
    this.browserTime = new Date().toLocaleTimeString('en-US');
  }
}
Enter fullscreen mode Exit fullscreen mode

When you use Angular Universal, components will always be rendered in the browser but sometimes they will first be rendered on the server. Sometimes you have to know where the component is being rendered when you’re writing code and for that you use isPlatformBrowser and isPlatformServer.

What we are doing here is simply setting the time of render for demonstration purposes. We set the time the component is rendered on the server and the time the component is rendered on the browser. We also log a message to the console to track when the component is being rendered on the server.

When the component is rendered in the browser, it replaces what was there previously. So in order to preserve the server render time, we have to use TransferState. With TransferStatewe can remember and transfer data between the server and browser renders.

Add the following in app.component.html, just above the router-outlet, so that we can see the browser and server times.

<div style="margin-top: 25px">
  Time from the
  <span style="font-weight: 900; color: red; margin-right: 15px">BROWSER:</span>
  {{ browserTime }}
</div>
<div style="margin-top: 25px">
  Time from the
  <span style="font-weight: 900; color: red; margin-right: 15px">SERVER:</span>
  {{ serverTime }}
</div>
Enter fullscreen mode Exit fullscreen mode

Now refresh the page. The server time shows up as soon as the page loads and reflects the time when the full page was rendered on the server. The browser time however, does not appear until the JavaScript is loaded. It reflects the time the page was rendered by JavaScript in the browser. Now that we have JavaScript, we can continually update it like a clock.

What happens if we refresh the page? Will the server time update or stay the same? The server time updates because every time you refresh, the full page is regenerated on the server:


Screenshot showing browser and server times

Every time you refresh the page, you will observe the same behavior. But note that as you navigate to different routes, there is no console message and no server activity (aside from the favorite icon and that’s only because we have disabled the cache). Once the JavaScript loads, the application is a SPA just like normal. Click the ‘Heroes’ button to navigate to the heroes route to see for yourself.

But if you enter the path to a different route in the address bar, it will request the fully rendered page for that route from the server. Enter: http://localhost:4200/heroes to confirm:


Screenshot showing console message that component was rendered on the server

Prerendering

Angular Universal comes with prerendering built-in. Prerendering means generating full static HTML pages for routes at build time. This reduces the initial load time even more since the browser does not have to wait for the server to generate the page when it requests it.

The downside is that in order to update the page you have to do another build. And of course, if you need data from the request in order to generate the page (like a userId) prerendering would not be an option.

By default, Angular Universal will try to guess all of the non-dynamic routes and generate pages for them. But you can turn this off and instead specify routes explicitly in a route.txt file or in the angular.json file. This is essentially hybrid SSR/SSG.

To prerender, stop the running app and execute:

npm run prerender
Enter fullscreen mode Exit fullscreen mode

Note that the console message telling us that the component is being rendered on the server displayed three times — once for each route (/, /dashboard, /heroes) .


Console message showing three routes prerendered

Take a look in the dist/angular.io-example/browser folder and you will see three index.html files — again, one for each route:


Screenshot showing 3 HTML files

Our browser/server time display is in the app component and so the HTML displaying the times got duplicated on all three pages:


Screenshot showing duplicated HTML

Now execute:

npm run serve:ssr
Enter fullscreen mode Exit fullscreen mode

The app is now running on port 4000, so navigate to http://localhost:4000/. Note that we did not get a console message telling us that the component is being rendered on the server when the page was requested and delivered.

Now what will happen when we refresh the page? Will the server time update or remain the same? It remains the same. The page is only generated on the server once at build time so the server time does not update.


Screenshot showing that the server time does not update when you refresh the page

Now we have an Angular application where we can generate full HTML pages for routes at build time, which is essentially SSG.

Potential Gotchas

Before we leave, I want to highlight one of the potential gotchas with SSG. If you click on a specific hero, that will take you to a dynamic detail route and there is no server activity since the app is behaving as a SPA. But if you enter one of those detail routes (e.g., http://localhost:4000/detail/13) in the address bar, the page will get generated on the server since that is a dynamic route that was not generated by default.


Screenshot showing that the server time is updated if you request a detail route page

Now click the ‘Dashboard’ button. The server time still shows as 9:05 but this is not the time that the ‘Dashboard’ page was prerendered on the server — it’s the time the detail route was generated on the server.

Now refresh the page. The server time reverts back to the correct prerendered value for the dashboard route.


Screen shot showing that the server time reverts to the prerendered value on page refresh

This trivial example highlights one of the challenges that come with SSG and that is data consistency. If you are implementing SSG for an application that has dynamic data, you have to be careful and really think through how you are retrieving and displaying data. This is one of the reasons that you generally do not want to implement SSG unless your application needs it.

Of course, it’s not all or nothing. You can choose SSG for static pages and SSR for pages that have dynamic data.

That’s it. In this post we walked through how to implement SSR and SSG for an Angular application. Hope you found this useful — happy coding!

Bibliography

💖 💪 🙅 🚩
seanbh
seanbh

Posted on August 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How to Implement Angular Universal (SSR/SSG)
angularuniversal How to Implement Angular Universal (SSR/SSG)

August 28, 2023