How I improved Angular performance and page responsiveness

[FRS-355] FusionReactor Production Debugger fails to start due to missing GLIBC_2.14
[FRS-418] FusionReactor Cloud Firewall DNS and Static IP address rules (FusionReactor 5.0.x – 7.4.x)

How I improved Angular performance and page responsiveness

For a little while now I’ve had issues with DOM rendering performance within an enterprise-scale product, built using Angular. I’ve always tried to follow some common approaches[note]”3 Tips for Angular Runtime Performance from the Real World.” https://blog.angular.io/3-tips-for-angular-runtime-performance-from-the-real-world-d467fbc8f66e. Accessed 29 Nov. 2019.[/note] to improving and maintaining high performance within an Angular application.

The main approaches I’ve taken to combat performance degradation over time within this application are as follows;

Working outside the Angular zone

	/**
	 * Loop outside of the Angular zone
	 * so the UI does not refresh after each setTimeout cycle
	 */
	logOutsideOfAngularZone() {
		this.ngZone.runOutsideAngular(() => {
			setTimeout(() => {
				// reenter the Angular zone and display a console log
				this.ngZone.run(() => { console.log('Outside Done!'); });
			});
		});
	}

Adjusting ChangeDetection Strategies

@Component({
	selector       : 'app-my-component',
	template       : `
		<h1>Title</h1>
	`,
	changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent implements OnInit, OnDestroy
{
	constructor(private cdRef: ChangeDetectorRef) {}
	public ngOnInit(): void {}
}

Using trackBy Functions with *ngFor

@Component({
	selector       : 'app-my-component',
	template       : `
		<h1>Title</h1>

		<li *ngFor="let person of people; trackBy:trackByFunction">{{person.name}}</li>
	`,
})
export class MyComponent implements OnInit, OnDestroy
{
	public people: any[] = [
		{ id: 123, name: 'John' },
		{ id: 456, name: 'Doe' },
	];

	constructor() {}
	public ngOnInit(): void {}

	public trackByFunction = (index: number, person: any): number => person.id;
}

While using all these techniques listed above, did result in isolated and localized increases in page performance, I still suffered from an overall application-wide DOM rendering performance problem. This was perceivable to me as what I consider to be page rendering lag whereby elements of the page are visible at given dimensions and positioned a given way, then ping to their correct position and dimensions. Another visible indicator of this issue was noticeable delays in mouse hover queues, such as subtle underlines, CSS animations, and tooltip display.

The Culprit! my position-to-bottom directive

After further investigation, I discovered that one thing this product made use of that my other products did not, was a directive, the position-to-bottom directive.

export class PositionToBottomDirective implements OnDestroy, AfterViewInit
{
	private readonly ngUnsubscribe: Subject<any> = new Subject();

	constructor(private readonly el: ElementRef, private readonly zone: NgZone) {
		this.el.nativeElement.style.height = 'auto';
	}

	public ngOnDestroy(): void {
		this.ngUnsubscribe.next();
		this.ngUnsubscribe.complete();
	}

	public ngAfterViewInit(): void {
		setTimeout(() => this.calcSize());

		this.zone.runOutsideAngular(() => {
			fromEvent(window, 'resize')
				.pipe(debounceTime(500), takeUntil(this.ngUnsubscribe))
				.subscribe((res: any) => this.calcSize());
		});
	}

	public calcSize(): void {
		let viewport: { top: string };

		this.zone.runOutsideAngular(() => {
			viewport = this.el.nativeElement.getBoundingClientRect()
		});

		const offset: number = parseInt(viewport.top, 10) + 10;
		const height: string = `calc(100vh - ${ offset }px)`;

		this.el.nativeElement.style.overflowY = 'auto';
		this.el.nativeElement.style.height = height;
	}

From the code snippet above, you can see that this directive was relatively simple. Upon initialization and after every browser resizes event, each component this directive was attached to would have it’s height set to the available space within the window.

Use of the RxJS debounceTime[note]”debounceTime · learn-rxjs.” https://www.learnrxjs.io/operators/filtering/debouncetime.html. Accessed 29 Nov. 2019.[/note] and Angular’s runOutsideAngular[note]”NgZone – Angular.” https://angular.io/api/core/NgZone. Accessed 29 Nov. 2019.[/note] the functionality I had hoped to mitigate the impact this directive would have on the performance of the product, as I knew Angular’s change detection will be called for every asynchronous browser event[note]”Angular Change Detection – How Does It Really Work?.” 26 Apr. 2019, https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/. Accessed 29 Nov. 2019.[/note].

Unfortunately, this was not enough so I removed the use of this directive in favor of CSS Flexbox (probably should have used this to begin with :D). After removing the use of this directive I saw a 61% increase in page responsiveness. This was calculated using the top 10 Total time-consuming activities.

How I improved Angular performance and page responsiveness, FusionReactor

Top 10 time-consuming activities before removal of this directive

How I improved Angular performance and page responsiveness, FusionReactor

Top 10 time-consuming activities after removal of this directive