Build Faster JavaScript Web Apps with Angular Universal, a TransferState Service and an API Watchdog
Time to read: 8 minutes
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:
When the project is initialized, navigate to its directory:
And run the application by typing:
You should see following output in the console:
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:
We can check to see if Angular Universal is working correctly by running our app and performing a curl request on it:
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.
You can run the following commands to catch up to this step in the project:
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:
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:
Initialize the npm application and install dependencies by running the following command in the externalApi
directory:
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:
Place the CustomService
implementation inside src/app/custom.service.ts
:
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:
Create two components that will display responses from our service. First, one for the fast
endpoint:
Place following code inside src/app/fast/fast.component.ts
:
And second, one for the “delayed” slow
endpoint:
Place the following code inside src/app/slow/slow.component.ts
:
Application Routing
Replace the code in the src/app/app-routing.module.ts
file with:
Place navigation links in the src/app/app.component.html
:
Use the following commands if you want to catch up to this step in the project:
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:
In another console window, build and run the application in the angular-universal-transfer-state
directory:
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):
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:
Replace the contents of src/app/http-interceptor.service.ts
with this code:
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:
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 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:
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:
In the server-side code we want to perform the call and store its response in the TransferState
registry:
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:
We can provide the HTTP interceptor in the src/app/app.module.ts
by replacing the existing code with the following:
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:
Include BrowserTransferStateModule
in the browser-side module by replacing the code in src/app/app.browser.module.ts
with the following code:
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:
Test the TransferState service
Recompile the application and check how many calls are made to the back end.
Build and run the 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:
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:
And import TransferHttpCacheModule
module into src/app/app.module.ts
by replacing the contents with the following code:
All other steps remains same. If you want to catch up to this step, run:
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:
And replace the code in src/app/slow-component-resolver.service.ts
with the following:
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 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:
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.
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:
Update the “slow” component to use the Route Resolver by replacing the code in src/app/slow/slow.component.ts
with the following:
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:
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.