Angular Universal: real app problems

Post Editor

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

8 min read
3 comments
post

Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

post
post
8 min read
3 comments
3 comments

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular.

Angular Universal supports multiple backends:

  1. Express
  2. ASP.NET Core
  3. hapi

Another package Socket Engine is a framework-agnostic that theoretically allows any backend to be connected to an SSR server.

This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal and Express.


How Angular Universal Works

For rendering on the server, Angular uses the DOM implementation for node.js — domino. For each GET request, domino creates a similar Browser Document object. In that object context, Angular initializes the application. The app makes requests to the backend, performs various asynchronous tasks, and applies any change detection from components to the DOM while still running inside node.js environment. The render engine then serializes DOM into a string and serves up the string to the server. The server sends this HTML as a response to the GET request. Angular application on the server is destroyed after rendering.

SSR issues in Angular

1. Infinite page loading
Link to this section

Situation
Link to this section

The user opens a page on your site and sees a white screen. In other words, the time until the first byte takes too long. The browser really wants to receive a response from the server, but the request ends up with a timeout.

Why is this happening
Link to this section

Most likely, the problem lies in the Angular-specific SSR mechanism. Before we understand at what point the page is rendered, let's define Zone.js andApplicationRef.

Zone.js is a tool that allows you to track asynchronous operations. With its help, Angular creates its own zone and launches the application in it. At the end of each asynchronous operation in the Angular zone, change detection is triggered.

ApplicationRef is a reference to the running application (docs). Of all this class's functionality, we are interested in the ApplicationRef#isStable property. It is an Observable that emits a boolean. isStable is true when no asynchronous tasks are running in the Angular zone and false when there are any.

So, application stability is the state of the application, which depends on the presence of asynchronous tasks in the Angular zone.

So, at the moment of the first onset of stability, Angular renders the current state applications and destroys the platform. And the platform will destroy the application.

We can now assume that the user is trying to open an application that cannot achieve stability. setInterval, rxjs.interval or any other recursive asynchronous operation running in the Angular zone will make stability impossible. HTTP requests also affect stability. The lingering request on the server delays the moment the page is rendered.

Possible Solution
Link to this section

To avoid the situation with long requests, use the timeout operator from rxjs library:

<>Copy
import { timeout, catchError } from 'rxjs/operators'; import { of } from 'rxjs/observable/of'; http.get('<https://example.com>') .pipe( timeout(2000), catchError(e => of(null)) ).subscribe()

The operator will throw an exception after a specified period of time if no server response is received.

This approach has 2 cons:

  • there is no convenient division of logic by platform;
  • the timeout operator must be written manually for each request.

As a more straightforward solution, you can use the NgxSsrTimeoutModule module from the @ngx-ssr/timeout package. Import the module with the timeout value into the root module of the application. If the module is imported into AppServerModule, then HTTP request timeouts will only work for the server.

<>Copy
import { NgModule } from '@angular/core'; import { ServerModule, } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout'; @NgModule({ imports: [ AppModule, ServerModule, NgxSsrTimeoutModule.forRoot({ timeout: 500 }), ], bootstrap: [AppComponent], }) export class AppServerModule {}

Use the NgZone service to take asynchronous operations out of the Angular zone.

<>Copy
import { Injectable, NgZone } from "@angular/core"; @Injectable() export class SomeService { constructor(private ngZone: NgZone){ this.ngZone.runOutsideAngular(() => { interval(1).subscribe(() => { // somo code }) }); } }

To solve this problem, you can use the tuiZonefree from the@taiga-ui/cdk:

<>Copy
import { Injectable, NgZone } from "@angular/core"; import { tuiZonefree } from "@taiga-ui/cdk"; @Injectable() export class SomeService { constructor(private ngZone: NgZone){ interval(1).pipe(tuiZonefree(ngZone)).subscribe() } }

But there is a nuance. Any task must be interrupted when the application is destroyed. Otherwise, you can catch a memory leak (see issue #5). You also need to understand that tasks that are removed from the zone will not trigger change detection.

2. Lack of cache out of the box
Link to this section

Situation
Link to this section

The user loads the home page of the site. The server requests data for the master and renders it, spending 2 seconds on it. Then the user goes from the main to the child section. Then it tries to go back and waits for the same 2 seconds as the first time.

If we assume that the data on which the main render depends has not changed, it turns out that HTML with this set has already been rendered. And in theory, we can reuse the HTML we got earlier.

Possible Solution
Link to this section

Various caching techniques come to the rescue. We'll cover two: in-memory cache and HTTP cache.

HTTP cache. When using a network cache, it's all about setting the correct response headers on the server. They specify the cache lifetime and caching policy:

<>Copy
Cache-Control: max-age = 31536000

This option is suitable for an unauthorized zone and in the presence of long unchanging data.

You can read more about the HTTP cache here

In-memory cache. The in-memory cache can be used for both rendered pages and API requests within the application itself. Both possibilities are package @ngx-ssr/cache.

Add the NgxSsrCacheModule module to the AppModule to cache API requests and on the server in the browser.

The maxSize property is responsible for the maximum cache size. A value of 50 means that the cache will contain more than 50 of the last GET requests made from the application.

The maxAge property is responsible for the cache lifetime. Specified in milliseconds.

<>Copy
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { NgxSsrCacheModule } from '@ngx-ssr/cache'; import { environment } from '../environments/environment'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, NgxSsrCacheModule.configLruCache({ maxAge: 10 * 60_000, maxSize: 50 }), ], bootstrap: [AppComponent], }) export class AppModule {}

You can go ahead and cache the HTML itself.

For example, everything in the same package @ngx-ssr/cache has a submodule@ngx-ssr/cache/express. It imports a single withCache function. The function is a wrapper over the render engine.

<>Copy
import { ngExpressEngine } from '@nguniversal/express-engine'; import { LRUCache } from '@ngx-ssr/cache'; import { withCache } from '@ngx-ssr/cache/express'; server.engine( 'html', withCache( new LRUCache({ maxAge: 10 * 60_000, maxSize: 100 }), ngExpressEngine({ bootstrap: AppServerModule, }) ) );

3. Server errors of type ReferenceError: localStorage is not defined
Link to this section

Situation
Link to this section

The developer calls localStorage right in the body of the service. It retrieves data from the local storage by key. But on the server, this code crashes with an error: ReferenceError: localStorage is undefined.

Why is this happening
Link to this section

When running an Angular application on a server, the standard browser API is missing from the global space. For example, there's no global object document like you'd expect in a browser environment. To get the reference to the document, you must use the DOCUMENT token and DI.

Possible Solution
Link to this section

Don't use the browser API through the global space. There is DI for this. Through DI, you can replace or disable browser implementations for their safe use on the server.

The Web API for Angular can be used to resolve this issue.

For example:

<>Copy
import {Component, Inject, NgModule} from '@angular/core'; import {LOCAL_STORAGE} from '@ng-web-apis/common'; @Component({...}) export class SomeComponent { constructor(@Inject(LOCAL_STORAGE) localStorage: Storage) { localStorage.getItem('key'); } }

The example above uses the LOCAL_STORAGE token from the @ng-web-apis/common package. But when we run this code on the server, we will get an error from the description. Just add UNIVERSAL_LOCAL_STORAGE from the package @ng-web-apis/universal in the providersAppServerModule, and by the token LOCAL_STORAGE, you will receive an implementation of localStorage for the server.

<>Copy
import { NgModule } from '@angular/core'; import { ServerModule, } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { UNIVERSAL_LOCAL_STORAGE } from '@ngx-ssr/timeout'; @NgModule({ imports: [ AppModule, ServerModule, ], providers: [UNIVERSAL_LOCAL_STORAGE], bootstrap: [AppComponent], }) export class AppServerModule {}

4. Inconvenient separation of logic
Link to this section

Situation
Link to this section

If you need to render the block only in the browser, you need to write approximately the following code:

<>Copy
@Component({ selector: 'ram-root', template: '<some-сomp *ngIf="isServer"></some-сomp>', styleUrls: ['./app.component.less'], }) export class AppComponent { isServer = isPlatformServer(this.platformId); constructor(@Inject(PLATFORM_ID) private platformId: Object){} }

The component needs to get the PLATFORM_ID, target platform, and understand the class's public property. This property will be used in the template in conjunction with the ngIf directive.

Possible Solution
Link to this section

With the help of structural directives and DI, the above mechanism can be greatly simplified.

First, let's wrap the server definition in a token.

<>Copy
export const IS_SERVER_PLATFORM = new InjectionToken<boolean>('Is server?', { factory() { return isPlatformServer(inject(PLATFORM_ID)); }, });

Create a structured directive using the IS_SERVER_PLATFORM token with one simple target: render the component only on the server.

<>Copy
@Directive({ selector: '[ifIsServer]', }) export class IfIsServerDirective { constructor( @Inject(IS_SERVER_PLATFORM) isServer: boolean, templateRef: TemplateRef<any>, viewContainer: ViewContainerRef ) { if (isServer) { viewContainer.createEmbeddedView(templateRef); } } }

The code looks similar to the IfIsBowser directive.

Now let's refactor the component:

<>Copy
@Component({ selector: 'ram-root', template: '<some-сomp *ifIsServer"></some-сomp>', styleUrls: ['./app.component.less'], }) export class AppComponent {}

Extra properties have been removed from the component. The component template is now a bit simpler.

Such directives declaratively hide and show content depending on the platform.

We have collected the tokens and directives in the package @ngx-ssr/platform.

5. Memory Leak
Link to this section

Situation
Link to this section

At initialization, the service starts an interval and performs some actions.

<>Copy
import { Injectable, NgZone } from "@angular/core"; import { interval } from "rxjs"; @Injectable() export class LocationService { constructor(ngZone: NgZone) { ngZone.runOutsideAngular(() => interval(1000).subscribe(() => { ... })); } }

This code does not affect the application's stability, but the callback passed to subscribe will continue to be called if the application is destroyed on the server. Each launch of the application on the server will leave behind an artifact in the form of an interval. And this is a potential memory leak.

Possible Solution
Link to this section

In our case, the problem is solved by using the ngOnDestoroy hook. It works for both components and services. We need to save the subscription and terminate it when the service is destructed. There are many techniques for unsubscribing, but here is just one:

<>Copy
import { Injectable, NgZone, OnDestroy } from "@angular/core"; import { interval, Subscription } from "rxjs"; @Injectable() export class LocationService implements OnDestroy { private subscription: Subscription; constructor(ngZone: NgZone) { this.subscription = ngZone.runOutsideAngular(() => interval(1000).subscribe(() => {}) ); } ngOnDestroy(): void { this.subscription.unsubscribe(); } }

6. Lack of rehydration
Link to this section

Situation
Link to this section

The user's browser displays a page received from the server, a white screen flickers for a moment, and the application starts functioning and looks normal.

Why is this happening
Link to this section

Angular does not know how to reuse what it has rendered on the server. It strips all the HTML from the root element and starts painting all over again.

Possible Solution
Link to this section

It still doesn't exist. But there is hope that there will be a solution. Angular Universal's roadmap has a clause: "Full client rehydration strategy that reuses DOM elements/CSS rendered on the server".

7. Inability to abort rendering
Link to this section

Situation
Link to this section

We are catching a critical error. Rendering and waiting for stability are meaningless. You need to interrupt the process and give the client the default index.html.

Why is this happening
Link to this section

Let's go back to the moment of rendering the application. It occurs when the application becomes stable. We can make our application stable faster using the solution from problem #1. But what if we want to abort the rendering process on the first caught error? What if we want to set a time limit on trying to render an application?

Possible Solution
Link to this section

There is no solution to this problem now.

Summary

In fact, Angular Universal is the only supported and most widely used solution for rendering Angular applications on the server. The difficulty of integrating into an existing application depends largely on the developer. There are still unresolved issues that don't allow me to classify Angular Universal as a production-ready solution. It is suitable for landing pages and static pages, but on complex applications, you can collect many problems, the solution of which will break in the blink of the page due to the lack of rehydration.

Comments (3)

authorpradeekumar
21 July 2021

Does SSR slow down the page performance?

authorSantoshah
14 October 2021

I am experienceing same.

authormaxkoretskyi
28 October 2021

hey, DM me on Twitter @maxkoretskyi

On Wed, 27 Oct 2021 at 21:15, Agustin Haller @.***> wrote:

@IKatsuba https://github.com/IKatsuba where can I read more about how do you handle attribution for the content generated by contributors to indepth.dev?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/indepth-dev/community/discussions/7#discussioncomment-1547900, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABOXEO5DKX72C25N47VFF53UJA6TDANCNFSM4ZKPE2MQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

authorVlad00b
9 November 2021

Hi. Has anyone faced such a problem https://github.com/Angular-RU/universal-starter/issues/272 ?

Share

About the author

author_image

Angular researcher, inDepth writer, frontend-developer, sometimes tech speaker, husband and father

author_image

About the author

Igor Katsuba

Angular researcher, inDepth writer, frontend-developer, sometimes tech speaker, husband and father

About the author

author_image

Angular researcher, inDepth writer, frontend-developer, sometimes tech speaker, husband and father

Looking for a JS job?
Job logo
Senior Full-Stack Developer (Node+Angular)

A-listware

Ukraine
Remote
$60k - $66k
Job logo
Full Stack Java/Angular Developer

Black Knight

Worldwide
Remote
$70k - $90k
Job logo
Angular Developer

Ziras Technologies

United States
Remote
$58k - $145k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles