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 approaches1 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 where by 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 resize 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 debounceTime2 and Angular’s runOutsideAngular3 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 event4.

Unfortunately this was not enough so I removed the 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 calculate using the top 10 Total time consuming activities.

Top 10 time consuming activities before removal of this directive
Top 10 time consuming activities after removal of this directive
  1. “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.
  2. “debounceTime · learn-rxjs.” https://www.learnrxjs.io/operators/filtering/debouncetime.html. Accessed 29 Nov. 2019.
  3. “NgZone – Angular.” https://angular.io/api/core/NgZone. Accessed 29 Nov. 2019.
  4. “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.

Comments are closed.