Skip to main content

Angular Universal + Caching (TransferState)





Angular Universal is a great solution for server-side rendering Angular application. It allows us to set meta tags for SEO, for sharing on social networks, but most importantly in renders our page on the server so the user gets already rendered page which means faster and smoother performance.
However, most of the time developers don’t use all advantages of SSR. For example, in some cases they block (isPlatFormBrowser) api calls, therefore, server renders a page without data, and client renders again with data in frontend (data is fetched in frontend) which leads to so called “flickering” glitch. In other cases the server (ssr), while rendering the page, does the request to the API, but the same request is done on the frontend as well, which in this time means double request for the data that has been already fetched from server.
Instead, all of this we should let the Universal to fetch all GET requests on the server, store it somewhere and when the client (frontend) will do the request, it will get already fetched data and won’t make any additional request to api server.

Default solution TransferHttpCacheModule

Yes, there is a dedicated module, TransferHttpCacheModule which does exactly what we want. It will register an interceptor and when the SSR fetches the data, it will save the data in the state and frontend will get that data, from the state, without making an additional request.
All you need to do is add TransferHttpCacheModule from @nguniversal/common inside imports array of your App module.
Then, import ServerTransferStateModule from @angular/platform-server in your Server module.
After this, if there are no blocking parts like checks for server/browser, you will see that the page gets rendered but there are no calls in the network tab, because Angular Universal already fetched the data and passed to fronted as a state. However, there’s one thing to consider. This is all it does, nothing more. In many cases, we want to write our own caching mechanism, which unfortunately is not possible with this module. However, the best part is we can write our own interceptor and change/get the state.

The problem

As an example (which was real issue for me) consider the following situation. When any user wants to open a public page with public data, SSR will do a request to API endpoint, get the data, render it and send back to the user. Now imagine you have a very large user base and the same page is requested 10–20 times in a second. Each second SSR will make a request, get mostly the same data and give it to transferState. This is already bad because we are making too many requests even tho we know that data most likely is the same. 
Now think that SSR will try to fetch some public data from third party server. Let's say a list of US presidents. And that third party server has limitation of 10 requests from one IP address in a second. Now if more than 10 people at the same time will try to open that page, SSR will make 10+ requests to third party api and most probably get banned. The thing is when all requests are handled by server, the server itself is just a computer with its own IP so all requests will be from the same IP address.
To solve this particular issue and the issue with requesting the same, unchanged data again and again we need to write our own logic for transferstate.
Caching and manual managing transferState
First let's write the same (or very similar) interceptor that is used in TransferHttpCacheModule .
  1. Create a new file serverstate.interceptor.ts
import {makeStateKey, TransferState} from '@angular/platform-browser'; @Injectable()export class ServerStateInterceptor implements HttpInterceptor { constructor(private transferState: TransferState) {} intercept(req: HttpRequest<any>, next: HttpHandler) { return next.handle(req).pipe( tap(event => { if (event instanceof HttpResponse) { this.transferState.set(makeStateKey(req.url), event.body); } }) ); 
When the SSR gets the data from the api, the interceptor will use the request url as the key and store the actual response body in a special object (transferState).
2. Register serverstate.interceptor.tsin Server module
3. Now all responses from SSR requests will be stored in transferState. We need to create another interceptor, this time for frontend so that instead of re-fetching the data, it will grab it from the state if it exists there.
Create new file browserstate.interceptor.ts
import { TransferState, makeStateKey } from '@angular/platform-browser';@Injectable({ providedIn: 'root'})export class BrowserStateInterceptor implements HttpInterceptor { constructor( private transferState: TransferState, ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (req.method !== 'GET') { return next.handle(req); } const storedResponse: string = this.transferState.get(makeStateKey(req.url), null); if (storedResponse) { const response = new HttpResponse({ body: storedResponse, status: 200 }); return of(response); } return next.handle(req); }}

First, we are checking if the request method is GET, if not we pass the request to the next interceptor or to http client.
Then we are trying to get saved data from transferState. If there is a data, we create a new HttpResponse object with our data and return it, so no other interceptor will intercept this request. If there is nothing in transferState for given key (url) we are passing request to next interceptor or http client to actually make the request.
4. Add BrowserStateInterceptor in your App module
5. And finally remove TransferHttpCacheModule from App module
So now we have almost the same features as TransferHttpCacheModule but with our custom solution. However this doesn’t solve our problem with caching and frequent requests.
Caching
The idea is simple. When a user opens a page for a first time , SSR will do a request to API server, get that data, save in transferState AND also save it with the same key (request url) in some local database. For next user requests we gonna check if for given key (request url) there is entry in our local database. If yes, we will save it in transferState and return it as HttpResponse, so that no actual request to API server will be made. If not we will repeat first step, fetch from API, save it in transferState and in local database.
You can implement your own logic for local server database (it even can be simple array or object). I’m going to use memory-cache node module.
npm i memory-cache
You may also want to install @types/memory-cache for better type checking.
Include it in your serverstate.interceptor.ts file
First lets modify logic inside next.handle() to add responses from API server in our local database
Change the following part (new added code is highlighted)
And before returning next.handle() we need to check our local DB and return value from there
With this we are almost ready. Now only the one request from SSR to API server will be made, and all users will get saved data from our SSR server local database. However, this brings us another issue. Probably we don’t want to save this data in our local db forever. At least we might want to update it sometimes. Well, we can create a function that will clear the all local DB or selected keys from there. But we need to trigger that function somehow (maybe with api call). Or we can do even better. We can set time and invalidate our local data after that time. memory-cache module has support of keys with expiration time. If you use custom solution you can implement this with setTimeout() .
Now lets modify the code again so that each response data stored in our local db will be valid for 5 minutes. During that 5 minutes all users will get data from our local database, hence no API calls will be made. After invalidating the local data, new request will trigger new API call, and new data will be stored in our local db for another 5 minutes and so on.
Now if you run your application you will see nothing is working !!! There will be infinite (actually it will take 5 minutes in this case) loading.
Actually the problem is not with memory-cache or your custom solution. The problem is with Angular Universal handling async code.
Angular Universal won’t complete rendering the page until there are unfinished async events
Our memory-cache module uses setTimeout() internally and in our case Universal will wait 5 minutes so that task queue will be empty and only after that it will finish its job.
As a final step, to solve this issue we can tell Angular to run this code outside of the Angular zone.
So now, instead of default and basic TransferHttpCacheModule we have fully functioning custom transferState mechanism that can be modified in many many other ways.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...