Search Engine Optimization (SEO) is vital for most web applications. You can build SEO-friendly Angular apps with Angular Universal, but what about the performance and efficiency of such an application? This post will show you how to build fast Angular apps that use client and server resources efficiency while providing server-side rendering (SSR) for SEO purposes.
In this post we will:
- Create an Angular application
- Add server-side rendering with Angular Universal
- Set up an
HTTP_INTERCEPTOR
with a TransferState service, to prevent duplicate calls to server resources - Create a route resolver to protect against slow external APIs.
To accomplish the tasks in this post you will need to install Node.js and npm (The Node.js installation will also install npm) as well as Angular CLI. cURL for macOS, Linux, or Windows 10 (included with build 1803 and later) and Git are referred to in the instructions but are not required.
To learn most effectively from this post you should have the following:
- Working knowledge of TypeScript and the Angular framework
- Familiarity with Angular observables and dependency injection
There is a companion project for this post available on GitHub. Each major step in this post has its own branch in the repository.
Create the Angular project
Every Angular project begins with installation and initialization of the packages. Type the following at the command prompt in the directory under which you would like to create the project directory:
ng new angular-universal-transfer-state --style css --routing true --directory angularApp
When the project is initialized, navigate to its directory:
cd angular-universal-transfer-state
And run the application by typing:
ng serve
You should see following output in the console:
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
Date: 2018-10-29T08:58:37.685Z
Hash: cb54e4608cfb1115882b
Time: 7682ms
chunk {main} main.js, main.js.map (main) 10.7 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 227 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.22 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 15.9 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.29 MB [initial] [rendered]
After opening the URL provided in the command output, http://localhost:4200, you should see the following in your browser:
Add server-side rendering with Angular Universal
Type the following at the command prompt to install the Angular Universal module:
ng add @ng-toolkit/universal
We can check to see if Angular Universal is working correctly by running our app and performing a curl request on it:
npm run build:prod;npm run server
curl http://localhost:8080
If you don’t want to use curl you can open the URL in a browser and inspect the page source. The results, as follows, should be the same.
Ellipsis (“...
”) in the code below indicates a section redacted for brevity.
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>angular-universal-i18n</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.3bb2a9d4949b7dc120a9.css"><style ng-transition="app-root">
</style></head>
<body>
<app-root _nghost-sc0="" ng-version="7.0.4"><div _ngcontent-sc0="" style="text-align:center">
<h1 _ngcontent-sc0=""> Welcome to angular-universal-i18n! </h1>
...
</div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2>
<ul _ngcontent-sc0="">
<li _ngcontent-sc0=""><h2 _ngcontent-sc0="">
<a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2>
</li>
<li _ngcontent-sc0=""><h2 _ngcontent-sc0="">
<a _ngcontent-sc0="" href="https://github.com/angular/angular-cli/wiki" rel="noopener" target="_blank">CLI Documentation</a></h2>
</li>
<li _ngcontent-sc0=""><h2 _ngcontent-sc0="">
<a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2>
</li></ul></app-root>
<script type="text/javascript" src="runtime.ec2944dd8b20ec099bf3.js"></script>
<script type="text/javascript" src="polyfills.c6871e56cb80756a5498.js"></script>
<script type="text/javascript" src="main.f27bf40180c4a8476e2e.js"></script>
<script id="app-root-state" type="application/json">{}</script></body></html>
You can run the following commands to catch up to this step in the project:
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git
cd angular-universal-transfer-state
git checkout step1
cd angularApp
npm install
npm run build:prod
npm run server
Create an external API
Most applications perform calls to one or more API, whether on the application’s own server or on a 3rd party host. Our application will make calls to a service we will create and run on the Node.js server at a port address (8081) that is different than both the application’s port (4200) and the server-side rendering port (8080).
Before creating the service we’ll build a simple Node.js application with two endpoints which we will consume inside the application using the service. Create an externalApi
directory outside of the angular-universal-transfer-state
application’s directory structure. In that directory create a file externalApi.js
(so the relative path would be: ../externalApi/externalApi.js
) and place the following code in it:
const express = require('express');
const app = express();
app.get('/api/fast', (req, res) => {
console.log('fast endpoint hit');
res.send({response: 'fast'});
});
app.get('/api/slow', (req, res) => {
setTimeout(() => {
console.log('slow endpoint hit');
res.send({response: 'slow'});
}, 5000);
});
app.listen(8081, () => {
console.log('Listening');
});
Because we are using the Express web framework for Node.js for serving content from this application, we need to initialize it as a npm project and install dependencies. Create a package.json
file in the externalApi
directory and place following content inside:
{
"name": "angular-universal-transfer-state",
"version": "0.0.0",
"scripts": {
"start": "node externalApi.js"
},
"private": true,
"dependencies": {
"express": "^4.16.4"
},
"devDependencies": {}
}
Initialize the npm application and install dependencies by running the following command in the externalApi
directory:
npm install
Call the External API
Now we are going to create a service that will consume the endpoints from the externalApi
we just created. Generate it by typing following command in the console in the angular-universal-transfer-state/angularApp
directory:
ng g s custom --spec false
Place the CustomService
implementation inside src/app/custom.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CustomService {
constructor(private http: HttpClient) {}
public getFast(): Observable<any> {
return this.http.get<any>('http://localhost:8081/api/fast');
}
public getSlow(): Observable<any> {
return this.http.get<any>('http://localhost:8081/api/slow');
}
}
We need to import a HttpClientModule
in our application because we are injecting a HttpClient
service into CustomService
. Replace content of the src/app/app.module.ts
file with the following code:
import { NgtUniversalModule } from '@ng-toolkit/universal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FastComponent } from './fast/fast.component';
import { SlowComponent } from './slow/slow.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent,
FastComponent,
SlowComponent
],
imports: [
CommonModule,
NgtUniversalModule,
AppRoutingModule,
HttpClientModule
]
})
export class AppModule { }
Create two components that will display responses from our service. First, one for the fast
endpoint:
ng g c fast -m app -s -t --spec false
Place following code inside src/app/fast/fast.component.ts
:
import { Component, OnInit } from '@angular/core';
import { CustomService } from '../custom.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-fast',
template: `
<p>
Response is: {{response | async | json}}
</p>
`,
styles: []
})
export class FastComponent {
public response: Observable<any> = this.service.getFast();
constructor(private service: CustomService) { }
}
And second, one for the “delayed” slow
endpoint:
ng g c slow -m app -s -t --spec false
Place the following code inside src/app/slow/slow.component.ts
:
import { Component } from '@angular/core';
import { CustomService } from '../custom.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-slow',
template: `
<p>
Response is: {{response | async | json}}
</p>
`,
styles: []
})
export class SlowComponent {
public response: Observable<any> = this.service.getSlow();
constructor(private service: CustomService) {}
}
Application Routing
Replace the code in the src/app/app-routing.module.ts
file with:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { FastComponent } from './fast/fast.component';
import { SlowComponent } from './slow/slow.component';
const routes: Routes = [
{path: '', redirectTo: 'fast', pathMatch: 'full'},
{path: 'fast', component: FastComponent},
{path: 'slow', component: SlowComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Place navigation links in the src/app/app.component.html
:
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<a routerLink='/fast'>fast</a>
<a routerLink='/slow'>slow</a>
</div>
<router-outlet></router-outlet>
Use the following commands if you want to catch up to this step in the project:
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git
cd angular-universal-transfer-state
git checkout step2
cd externalApi
npm install
cd ../angularApp
npm install
DRY(C): Don’t Repeat Your Calls
What we have so far is an Angular application that successfully performs calls to an external API. Thanks to Angular Universal, it’s also search engine optimized and responses from those calls are displayed in the server-side rendered build.
But there is one pitfall. Let’s perform some investigation around the API calls.
In one console window, run externalApi.js
in the externalApi
directory:
node externalApi.js
In another console window, build and run the application in the angular-universal-transfer-state
directory:
npm run build:prod
npm run server
Navigate to the application at http://localhost:8080 with your favorite browser. The home page view is rendered and data from the external API is retrieved. Let’s take a look what’s going on in the console window where externalApi
is running):
node externalApi.js
Listening
fast endpoint hit
fast endpoint hit
As you can see, we performed two calls to our API, hitting the fast
endpoint twice. How it could that be, when we opened the website only once?
That happens “thanks to” server-side rendering. Here is the sequence of events:
- User requests page from Node.js
- Node.js makes a call to the
externalApi
fast
endpoint while serving Angular to the client, - The externalApi fast endpoint returns a response and Node.js adds it to the generated HTML
- HTML and Angular JavaScript are sent to the browser
- Angular bootstraps in the browser and performs a call to the
externalApi
fast
endpoint again - The
externalApi
fast
endpoint response is returned to the browser and is placed in the application view.
The process can be viewed in the following illustration:
Do you think it’s not super efficient? I agree with you.
Transfer State Service
We will improve the efficiency of our app by creating the TransferState
service, a key-value registry exchanged between the Node.js server and the application rendered in the browser. We will use it through an HTTP_INTERCEPTOR
mechanism which will reside inside the HttpClient
service and which will manipulate the requests and responses.
Type following command to generate the new service:
ng g s HttpInterceptor --spec false
Replace the contents of src/app/http-interceptor.service.ts
with this code:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TransferState, makeStateKey, StateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
@Injectable({
providedIn: 'root'
})
export class HttpInterceptorService implements HttpInterceptor {
constructor(private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: any) {}
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method !== 'GET') {
return next.handle(request);
}
const key: StateKey<string> = makeStateKey<string>(request.url);
if (isPlatformServer(this.platformId)) {
return next.handle(request).pipe(tap((event) => {
this.transferState.set(key, (<HttpResponse<any>> event).body);
}));
} else {
const storedResponse = this.transferState.get<any>(key, null);
if (storedResponse) {
const response = new HttpResponse({body: storedResponse, status: 200});
this.transferState.remove(key);
return of(response);
} else {
return next.handle(request);
}
}
}
}
We put a lot of code here. Let’s discuss it.
Our service implements the HttpInterceptor
interface, so we need to implement a corresponding method:
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
This method will be called whenever any API call is performed on the HttpClient
service.
For simplicity in our demonstration, we want to use the TransferState
registry only for GET calls. We need to check to see if a call matches that criteria:
if (request.method !== 'GET') {
return next.handle(request);
}
If it does, we generate a key based on the request URL. We will use the key-value pair to store or retrieve the request response, depending whether the request is being handled on the server side or browser side:
const key: StateKey<string> = makeStateKey<string>(request.url);
To differentiate between the server and the browser we are using the isPlatformServer
method from the @angular/common
library together with the PLATFORM_ID
injection token:
if (isPlatformServer(this.platformId)) {
//serverSide
} else {
//browserSide
}
In the server-side code we want to perform the call and store its response in the TransferState
registry:
if (isPlatformServer(this.platformId)) {
return next.handle(request).pipe(tap((event) => {
this.transferState.set(key, (<HttpResponse<any>> event).body);
}));
In the browser-side code we want to check to see if the response for a given call already resides in the registry. If it does, we want to retrieve it, clear the registry (so future calls can store fresh data), and return the response to the caller (CustomService
in this case). If the given key doesn’t exist in the registry we simply perform the HTTP call:
else {
const storedResponse = this.transferState.get<any>(key, null);
if (storedResponse) {
const response = new HttpResponse({body: storedResponse, status: 200});
this.transferState.remove(key);
return of(response);
} else {
return next.handle(request);
}
}
We can provide the HTTP interceptor in the src/app/app.module.ts
by replacing the existing code with the following:
import { NgtUniversalModule } from '@ng-toolkit/universal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FastComponent } from './fast/fast.component';
import { SlowComponent } from './slow/slow.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpInterceptorService } from './http-interceptor.service';
@NgModule({
declarations: [
AppComponent,
FastComponent,
SlowComponent
],
imports: [
CommonModule,
NgtUniversalModule,
AppRoutingModule,
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: HttpInterceptorService,
multi: true
}
]
})
export class AppModule { }
We need to import two new modules which contain the TransferState
service into our application. Include ServerTransferStateModule
in the server-side module by replacing the existing code in src/app/app.server.module.ts
with the following:
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import {NgModule} from '@angular/core';
import {ServerModule, ServerTransferStateModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';
import { BrowserModule } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({appId: 'app-root'}),
AppModule,
ServerModule,
NoopAnimationsModule,
ModuleMapLoaderModule,
ServerTransferStateModule
]
})
export class AppServerModule {}
Include BrowserTransferStateModule
in the browser-side module by replacing the code in src/app/app.browser.module.ts
with the following code:
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { NgModule } from '@angular/core';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({appId: 'app-root'}),
AppModule,
BrowserTransferStateModule
]
})
export class AppBrowserModule {}
The code in the src/main.ts
file also needs to be changed. We need to bootstrap our app in a slightly different way to make the TransferState
registry work properly; we need to bootstrap our app when the DOMContentLoaded
event is emitted by the browser. Replace the existing code with the following:
import { AppBrowserModule } from '.././src/app/app.browser.module';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic()
.bootstrapModule(AppBrowserModule)
.catch(err => console.log(err));
});
Test the TransferState service
Recompile the application and check how many calls are made to the back end.
Build and run the server:
npm run build:prod
npm run server
If you have stopped the externalApi
process for any reason you should restart it now.
Navigate to the second component, http://localhost:8080/slow, using the browser’s address bar. Doing this will perform the call against the server rather than running the local code (the Angular SPA) inside the browser, as clicking on the link on the home page would do.
Examine the output in the console window for the externalApi
process. It should look like the following, in which the first two fast endpoint responses are from the previous test and the last line is the result of the current test:
Listening
fast endpoint hit
fast endpoint hit
slow endpoint hit
Mission complete! The response retrieved by the back end is passed to the browser within the TransferState
registry.
An alternative HTTP interceptor
As an alternative to creating the custom HTTP_INTERCEPTOR
, you can use the standard TransferHttpCacheModule
from the @nguniversal
library. This makes implementation more convenient, but it also imposes a constraint: you can’t make any changes to the standard library, so you would not be able to add functionality like the API watchdog we are going to create in a forthcoming step.
To implement the standard transfer cache, install the dependency:
npm install @nguniversal/common
And import TransferHttpCacheModule
module into src/app/app.module.ts
by replacing the contents with the following code:
import { NgtUniversalModule } from '@ng-toolkit/universal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FastComponent } from './fast/fast.component';
import { SlowComponent } from './slow/slow.component';
import { HttpClientModule } from '@angular/common/http';
import { TransferHttpCacheModule } from '@nguniversal/common';
@NgModule({
declarations: [
AppComponent,
FastComponent,
SlowComponent
],
imports: [
CommonModule,
NgtUniversalModule,
AppRoutingModule,
HttpClientModule,
TransferHttpCacheModule
]
})
export class AppModule { }
All other steps remains same. If you want to catch up to this step, run:
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git
cd angular-universal-transfer-state
git checkout step3
cd externalApi
npm install
cd ../angularApp
npm install
Implement a performance watchdog
There is one more thing which we need to consider. As you probably noticed, loading http://localhost:8080/slow takes a lot time. It’s definitely not SEO-friendly. While this is because we introduced a time delay for demonstration purposes when we created the external API, there are many real world examples of APIs that respond slowly or not at all.
We are going to solve this issue by using a RouteResolver
in the call to the SlowComponent
.
Generate it by entering following command in the console window you’re using to build and run the app:
ng g s SlowComponentResolver --spec false
And replace the code in src/app/slow-component-resolver.service.ts
with the following:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable, timer } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
import { CustomService } from './custom.service';
import { takeUntil } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class SlowComponentResolverService implements Resolve<any> {
constructor(private service: CustomService, @Inject(PLATFORM_ID) private platformId: any) { }
public resolve(): Observable<any> {
if (isPlatformBrowser(this.platformId)) {
return this.service.getSlow();
}
const watchdog: Observable<number> = timer(500);
return Observable.create(subject => {
this.service.getSlow().pipe(takeUntil(watchdog)).subscribe(response => {
subject.next(response);
subject.complete();
});
watchdog.subscribe(() => {
subject.next(null);
subject.complete();
});
});
}
}
Examine this code a little bit. We have a resolve
method that we need to implement because we are implementing the Resolve
interface. Inside the method we are checking to see if the code is executing on the browser or server. If the code is being executed in the browser it waits for the call as long as necessary by executing the call:
if (isPlatformBrowser(this.platformId)) {
return this.service.getSlow();
}
If the code is being executed in the Node.js server an observable, watchdog
, is created using the timer
method from the rxjs
library. The timer
method creates an observable which emits a value only once after given amount of time in milliseconds:
const watchdog: Observable<number> = timer(500);
We use this observable with the takeUntil
method, piped to the request call. If the observable emits a value before the API sends a response it pushes null
to the component. Otherwise it pushes the API response.
return Observable.create(subject => {
this.service.getSlow().pipe(takeUntil(watchdog)).subscribe(response => {
subject.next(response);
subject.complete();
});
watchdog.subscribe(() => {
subject.next(null);
subject.complete();
});
});
}
We need to update the application’s routing to use this resolver. Replace the contents of src/app/app-routing.module.ts
with the following:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { FastComponent } from './fast/fast.component';
import { SlowComponent } from './slow/slow.component';
import { SlowComponentResolverService } from './slow-component-resolver.service';
const routes: Routes = [
{path: '', redirectTo: 'fast', pathMatch: 'full'},
{path: 'fast', component: FastComponent},
{path: 'slow', component: SlowComponent, resolve: {response: SlowComponentResolverService}}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Update the “slow” component to use the Route Resolver by replacing the code in src/app/slow/slow.component.ts
with the following:
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-slow',
template: `
<p>
Response is: {{response | json}}
</p>
`,
styles: []
})
export class SlowComponent {
public response: any = this.router.snapshot.data.response;
constructor(private router: ActivatedRoute) {}
}
Rebuild application and check how long it takes to render the slow component now.
Much better! If the call takes longer than 0.5 seconds we abandon it and perform it again in the browser. That’s exactly what we were looking for.
In the following illustrations you can see a scheme of how our new architecture works.
When the API responds quickly the response is handled by the server:
And when the API doesn’t reply before the watchdog is activated we send the browser partially rendered HTML and repeat the call:

As an alternative approach, if you don’t want to setup a watchdog mechanism globally, you can do it by providing it inside the HTTP_INTERCEPTOR
.
If you want to catch up to this step, run:
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git
cd angular-universal-transfer-state
git checkout step4
npm install
npm run build:prod
npm run server
Summary
Today we covered an important challenge: improving the performance and efficiency of applications that implement server-side rendering. We did this in two ways: with the TransferState
service we are able to limit calls made to potentially slow APIs. We also implemented a watchdog mechanism to abandon long-running API calls which can adversely impact the total time a server needs to render a view. Both these techniques help improve the performance of Angular websites, which increases user satisfaction and helps the site score better in search engine ranking.
If you want to learn more about Angular Universal techniques, check out my other posts on the Twilio blog Getting Started with Serverless Angular Universal on AWS Lambda and Create search engine friendly internationalization for Angular apps with Angular Universal and the ngx-translate module.
The Git repository, for the code used in this post can be found here: https://github.com/maciejtreder/angular-universal-transfer-state
I'm Maciej Treder, contact me via contact@maciejtreder.com, https://www.maciejtreder.com or @maciejtreder on GitHub, Twitter and LinkedIn.