Angular Universal: real app problems
Tags: angular, ssr, angularuniversal
Published: 2021-03-05 07:22:05

# 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.

Angular Universal supports multiple backends:

1. [Express](https://github.com/angular/universal/blob/master/modules/express-engine/README.md)
2. [ASP.NET Core](https://github.com/angular/universal/blob/master/modules/aspnetcore-engine/README.md)
3. [hapi](https://github.com/angular/universal/blob/master/modules/hapi-engine/README.md)

Another package [Socket Engine](https://github.com/angular/universal/tree/master/modules/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](https://www.npmjs.com/package/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

### Situation

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

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` and`ApplicationRef`.

**[Zone.js](https://indepth.dev/posts/1059/do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular)** 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](https://angular.io/api/core/ApplicationRef)). Of all this class's functionality, we are interested in the [ApplicationRef#isStable](https://angular.io/api/core/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 no such tasks.

**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](https://github.com/angular/angular/blob/11.1.x/packages/platform-server/src/utils.ts#L43), Angular [renders the current state](https://github.com/angular/angular/blob/11.1.x/packages/platform-server/src/utils.ts#L69) applications and [destroys the platform](https://github.com/angular/angular/blob/11.1.x/packages/platform-server/src/utils.ts#L70). 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

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

```tsx
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](https://github.com/IKatsuba/ngx-ssr) 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.

```tsx
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](https://angular.io/api/core/NgZone) service to take asynchronous operations out of the Angular zone.

```tsx
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](https://github.com/TinkoffCreditSystems/taiga-ui/blob/main/projects/cdk/observables/zone-free.ts#L30) from the`@taiga-ui/cdk`:

```tsx
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

### Situation

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

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:

```tsx
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)

**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.

```tsx
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.

```tsx
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

### Situation

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

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

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](https://ng-web-apis.github.io/) can be used to resolve this issue.

For example:

```tsx
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](https://github.com/ng-web-apis/common#tokens) 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](https://github.com/ng-web-apis/universal) in the providers`AppServerModule`, and by the token `LOCAL_STORAGE`, you will receive an implementation of localStorage for the server.

```tsx
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

### Situation

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

```tsx
@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

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.

```tsx
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.

```tsx
@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:

```tsx
@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

### Situation

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

```tsx
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

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:

```tsx
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

### Situation

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

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

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

### Situation

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

Let's go back to [the moment of rendering the application](https://github.com/angular/angular/blob/11.1.x/packages/platform-server/src/utils.ts#L43). 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

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.