Build Faster JavaScript Web Apps with Angular Universal, a TransferState Service and an API Watchdog
Time to read:
This post is part of Twilio’s archive and may contain outdated information. We’re always building something new, so be sure to check out our latest posts for the most up-to-date insights.
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_INTERCEPTORwith 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
externalApifastendpoint 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
externalApifastendpoint again - The
externalApifastendpoint 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.