Angular Performance: Dom Event Causes Unnecessary Function Calls

Angular Performance: DOM Event causes unnecessary function calls

Since you're calling a function in one of the data-binding syntaxes, whenever Angular performs Change Detection, it will call this method.

Before for a function, anything that cases is the value that it returns. And for Angular to know that the returned value has changed, Angular will have to run it.

This is the exact same issue that people have raised a several questions here:

  1. Angular: Prevent DomSanizer from updating on DOM Events

  2. Angular performance: ngStyle recalculates on each click on random input

  3. Angular 7 ,Reactive Form slow response when has large data

You might want to read through these threads to understand what's going on here and how you can fix this issue.


The solution is to basically design your implementation in such a way, that it never calls a method in one of the data-binding syntaxes, i.e.

  1. In String Interpolation - {{ methodCall() }}
  2. In Property Binding - [propertyName]="methodCall()"
  3. In Attribute Binding - [class.className]="methodCall()" / [style.style-name]="methodCall()"

An alternative solution is do move this code to a Child Component and configure the changeDetectionStrategy on that child component to ChangeDetectionStrategy.OnPush

Angular performance: ngStyle recalculates on each click on random input

That makes perfect sense. This is how Angular performs change detection. And this is Angular performing extra checks since you called a function in one of the data-binding syntaxes, here:

[ngStyle]="{'background-color': getBG(row*col)}" 

Angular performs Change Detection in three cases:

  1. DOM Events.
  2. AJAX Calls.
  3. Timeouts / Intervals.

This is the case of DOM Events (click).

Now when performing Change Detection, Angular check whether a particular variable in the Component has changed.

That's pretty straight forward in case of properties. But not so straight-forward in case of functions.

You see, the only way to determine whether the value of a function has changed is by calling it.

So Angular is doing just that.

SOLUTION:

Just create a matrix for the number to show and the color to paint right in the Component Class:

import { Component } from '@angular/core';

@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
rows = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
cols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
matrix = [];

model1 = '';
model2 = '';
model3 = '';
model4 = '';
model5 = '';

ngOnInit() {
this.rows.forEach((row, rowIndex) => {
this.matrix.push([]);
this.cols.forEach((col, colIndex) => {
const product = row * col;
this.matrix[row].push({
numberToShow: product,
color: this.getBG(product),
});
})
});
}

getBG(hue: number): string {
console.log('getBG was called');
return 'hsl(' + hue + ', 100%, 50%)';
}

}

And then use it in your template:

<br/>
<div> 1. Open a console</div>
<br/>

<section>
<div class="row" *ngFor="let row of matrix">
<div
class="col"
[style.background-color]="col.color"
*ngFor="let col of row ">
{{col.numberToShow}}
</div>
</div>
</section>

<br/>
<div>2. Click fast on the different inputs: </div>
<br/>

<section>
<input type="text" [ngModel]="model1"/>
<input type="text"[ngModel]="model2"/>
<input type="text"[ngModel]="model3"/>
<input type="text"[ngModel]="model4"/>
<input type="text"[ngModel]="model5"/>
</section>

Difference in the performance:

In the previous implementation, the getBG was called 401 times on initialization.

In the solution implementation, the getBG is called 101 times on initialization.

That's a massive performance gain of around 397%.

Plus there's no extra call to the getBG method when the user focuses and blurs out from any input field.

Here's a Working Sample StackBlitz for your ref.

You might also want to read through a Medium Article that I wrote about Performant Reactive Forms in Angular. Although it's related to Reactive Forms, I've touched upon this aspect, in the article. I'm sure you'll find it helpful.

Weird update behavior: Template constantly calls display function

This is expected behavior. This is data bound to the template, and Angular's change detection cycles check bound properties to see if their value has changed and need to be updated in the component or its view. Since this is a method, the only way Angular can check to see if its value has changed is by calling it and comparing the new result to the current value.

In many articles, you'll often see this behavior cited as bad practice because multiple semi-expensive method calls can significantly harm the speed of the page. However, a very simple method that returns a value with little or no calculation is hardly any different than using getters or native value accessors, so this practice isn't always bad...but, it's still (slightly) more work on the stack and will hold up change detection until the method completes.

For this reason, it's often considered best practice to either unwrap the value to a component property before binding or to use a pipe. If the value is going to be changing due to events outside of the component, an Observable with an async pipe is the most preferred pattern; the Observables emissions mean reactive change detection as opposed to polling.

You can also strategically tell Angular to turn off or restrict the change detection for the entire component and then use the ChangeDetectorRef to manually run a change detection cycle if needed...while that's a speedup overall, change detection is one of the most appealing things about Angular as a framework, and it's usually a better design choice to invert that control to Angular when you can.

Angular 7 ,Reactive Form slow response when has large data

So after about a day of playing around with your StackBlitz, here I am with the solution. I think this would significantly improve the performance.

Step 1: Create interfaces for your Data Model

Doing that would significantly make the code cleaner and more readable. It would also make the code more manageable and easy to work with. So here we go with the list of interfaces for your specific scenario:

export interface Hotel {
id: string;
currencyId: string;
hotelYearId: string;
priceTaxTypeId: string;
code: string;
name: string;
createBy: string;
createDate: string;
lastUpdateBy: string;
lastUpdateDate: string;
remark: string;
internalRemark: string;
roomTypes: RoomType[];
}

export interface RoomType {
chk: boolean;
roomTypeId: string;
mealTypes: MealType[];
}

export interface MealType {
chk: boolean;
mealTypeId: string;
marketGroups: MarketGroup[];
}

export interface MarketGroup {
chk: boolean;
markets: Market[];
rateSegments: RateSegment[];
}

export interface Market {
marketId: string;
}

export interface RateSegment {
chk: boolean;
rateSegmentId: string;
hotelSeasons: HotelSeason[];
}

export interface HotelSeason {
chk: boolean;
hotelSeasonId: string;
rates: Rate[];
}

export interface Rate {
rateCodeId: string;
cancellationPolicyId: string;
dayFlag: string;
singlePrice: string;
doublePrice: string;
xbedPrice: string;
xbedChildPrice: string;
bfPrice: string;
bfChildPrice: string;
unitMonth: string;
unitDay: string;
minStay: number;
}

Step 2: Change the way you're creating the form

The way you're creating the form is extremely noisy. There's a clear way of doing that. And since you're already creating the form in the service, I suggest you keep the task of creating the form to the service itself and keep your component free from any such task. So your service can be refactored like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators } from '@angular/forms';
import { map } from 'rxjs/operators';

import { Hotel, RoomType, MealType, MarketGroup, Market, RateSegment, HotelSeason, Rate } from './hotel.model';

@Injectable()
export class UtilService {

constructor(
private readonly fb: FormBuilder,
private readonly http: HttpClient
) { }

getHotelForm() {
return this.getHotel().pipe(
map((apiResponse: any) => this.fb.group({
id: [apiResponse.id, Validators.required],
currencyId: [apiResponse.currencyId, Validators.required],
hotelYearId: [apiResponse.hotelYearId, Validators.required],
priceTaxTypeId: [apiResponse.priceTaxTypeId, Validators.required],
code: [apiResponse.code, Validators.required],
name: [apiResponse.name, Validators.required],
createBy: [apiResponse.createBy, Validators.required],
createDate: [apiResponse.createDate, Validators.required],
lastUpdateBy: [apiResponse.lastUpdateBy, Validators.required],
lastUpdateDate: [apiResponse.lastUpdateDate, Validators.required],
remark: [apiResponse.remark, Validators.required],
internalRemark: [apiResponse.internalRemark, Validators.required],
roomTypes: this.fb.array(apiResponse.roomTypes.map(roomType => this.generateRoomTypeForm(roomType)))
}))
);
}

private getHotel() {
return this.http.get('/assets/hotel.json');
}

private generateRoomTypeForm(roomType: RoomType) {

const roomTypeForm = this.fb.group({
chk: [roomType.chk, Validators.required],
roomTypeId: [roomType.roomTypeId, Validators.required],
mealTypes: this.fb.array(roomType.mealTypes.map(mealType => this.generateMealTypeForm(mealType)))
});

return roomTypeForm;
}

private generateMealTypeForm(mealType: MealType) {

const mealTypeForm = this.fb.group({
chk: [mealType.chk, Validators.required],
mealTypeId: [mealType.mealTypeId, Validators.required],
marketGroups: this.fb.array(mealType.marketGroups.map(marketGroup => this.generateMarketGroupForm(marketGroup)))
});

return mealTypeForm;
}

private generateMarketGroupForm(marketGroup: MarketGroup) {

const marketGroupForm = this.fb.group({
chk: [marketGroup.chk, Validators.required],
markets: this.fb.array(marketGroup.markets.map(market => this.generateMarketForm(market))),
rateSegments: this.fb.array(marketGroup.rateSegments.map(rateSegment => this.generateRateSegmentForm(rateSegment))),
});

return marketGroupForm;
}

private generateMarketForm(market: Market) {
return this.fb.group({
marketId: [market.marketId, Validators.required]
});
}

private generateRateSegmentForm(rateSegment: RateSegment) {
const rateSegmentForm = this.fb.group({
chk: [rateSegment.chk, Validators.required],
rateSegmentId: [rateSegment.rateSegmentId, Validators.required],
hotelSeasons: this.fb.array(rateSegment.hotelSeasons.map(hotelSeason => this.generateHotelSeasonForm(hotelSeason)))
});

return rateSegmentForm;
}

private generateHotelSeasonForm(hotelSeason: HotelSeason) {

const hotelSeasonForm = this.fb.group({
chk: [hotelSeason.chk, Validators.required],
hotelSeasonId: [hotelSeason.hotelSeasonId, Validators.required],
rates: this.fb.array(hotelSeason.rates.map(rate => this.generateRateForm(rate)))
});
return hotelSeasonForm;
}

private generateRateForm(rate: Rate) {
return this.fb.group({
rateCodeId: [rate.rateCodeId, Validators.required],
cancellationPolicyId: [rate.cancellationPolicyId, Validators.required],
dayFlag: [rate.dayFlag, Validators.required],
singlePrice: [rate.singlePrice, Validators.required],
doublePrice: [rate.doublePrice, Validators.required],
xbedPrice: [rate.xbedPrice, Validators.required],
xbedChildPrice: [rate.xbedChildPrice, Validators.required],
bfPrice: [rate.bfPrice, Validators.required],
bfChildPrice: [rate.bfChildPrice, Validators.required],
unitMonth: [rate.unitMonth, Validators.required],
unitDay: [rate.unitDay, Validators.required],
minStay: [rate.minStay, Validators.required]
});
}

}

Step 3: Leverage the above service:

Do it to get the Form and get rid of the methods that would return to you the FormArrays in your template. That would make your Component very clean, clear and concise.

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';

import { UtilService } from '../app/util.service';

@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})

export class AppComponent {

form$: Observable<FormGroup> = this.util.getHotelForm();

constructor(private readonly util: UtilService) {
}

}

Step 4: Refactor your Template:

And this one is THE MOST IMPORTANT. NEVER call getters or methods in deeply nested forms to get the FormArrays. Or rather, in normal forms or inside a data binding syntax in general. Because they will get called in every single change detection cycle and would kill your App's performance.

Please refer to this lightning talk by Tanner Edwards from ng-conf 2018 to know more about it.

So, you can refactor your Component Template like this:

<form 
*ngIf="form$ | async as form"
[formGroup]="form">
<div
formArrayName="roomTypes">
<div
*ngFor="let roomType of form.controls['roomTypes'].controls; let index = index"
[formGroupName]="index">
{{index}}
<div
formArrayName="mealTypes">
<div
*ngFor="let mealType of roomType.controls['mealTypes'].controls; let mealtypeIndex = index"
[formGroupName]="mealtypeIndex">
mealtype {{mealtypeIndex}}
<div
formArrayName="marketGroups">
<div
*ngFor="let marketGroup of mealType.controls['marketGroups'].controls; let marketGroupIndex = index"
[formGroupName]="marketGroupIndex">
marketGroupIndex {{marketGroupIndex}}
<div formArrayName="rateSegments">
<div
*ngFor="let rateSegment of marketGroup.controls['rateSegments'].controls; let rateSegmentIndex = index"
[formGroupName]="rateSegmentIndex">
rateSegmentIndex {{rateSegmentIndex}}
<div formArrayName="hotelSeasons">
<div
class="fifth_border"
*ngFor="let hotelseason of rateSegment.controls['hotelSeasons'].controls; let hotelseasonIndex = index"
[formGroupName]="hotelseasonIndex">
hotelseasonIndex {{hotelseasonIndex}}
<div formArrayName="rates">
<div
*ngFor="let rate of hotelseason.controls['rates'].controls; let rateIndex = index"
[formGroupName]="rateIndex">
<div style="display:flex;flex-flow;row">
<div>
<p>SGL</p>
<input class="input text_right" type="text" formControlName="singlePrice">
</div>
<div>
<p>DLB/TWN</p>
<input class="input text_right" type="text" formControlName="doublePrice">
</div>
<div>
<p>EX-Adult</p>
<input class="input text_right" type="text" formControlName="xbedPrice" >
</div>
<div>
<p>EX-Child</p>
<input class="input text_right" type="text" formControlName="xbedChildPrice">
</div>
<div>
<p>Adult BF</p>
<input class="input text_right" type="text" formControlName="bfPrice">
</div>
<div>
<p>Child BF</p>
<input class="input text_right" type="text" formControlName="bfChildPrice">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <pre>{{form.value | json}}</pre> -->
</form>

Step 5: Don't stop here

This is not the end. It's just the beginning. You can also abstract the Child Form(the marketGroup FormGroup inside the marketGroups FormArray) into a separate component. And then you can set it's changeDetectionStrategy to OnPush. This would give you an even better performance.

Here's a StackBliz that you can refer to, to have a look at that solution.

Doing all this would significantly improve the performance of the form.

I hope this helps. I'll try to update this answer if I find anything else to improve the performance beyond this limit.



Here's a Working Sample StackBlitz for your ref.



Here's a Detailed Medium Article about this that I wrote for AngularInDepth.



Difference in Performance

I just did a Performance Audit on your Implementation as well as mine for the same set of steps performed on the App.

Here's the summary with your implementation:

Sample Image

And here's the summary with the refactored implementation:

Sample Image

Ways to improve AngularJS performance even with large number of DOM elements

I think that the best way to solve performance issues is to avoid using high level abstractions (AngularJS ng-repeat with all corresponding background magic) in such situations. AngularJS is not a silver bullet and it's perfectly working with low level libraries. If you like such functionality in a text block, you can create a directive, which will be container for text and incapsulate all low level logic. Example with custom directive, which uses letteringjs jquery plugin:

angular.module('myApp', [])
.directive('highlightZone', function () {
return {
restrict: 'C',
transclude: true,
template: '<div ng-transclude></div>',
link: function (scope, element) {
$(element).lettering('words')
}
}
})

http://jsfiddle.net/j6DkW/1/

Angular: Prevent DomSanizer from updating on DOM Events

The Problem:

The problem is caused because you've used a method in the data-binding syntax, here:

<iframe 
[src]="getURL()"
width="100%"
height="100px">
</iframe>

Because of this, every time Angular performs Change Detection, the getURL will get called.

The solution:

You can create a Pipe to sanitize the iframe src url. I've done something similar in this answer.

That way, your iframe won't reload every time Angular performs Change Detection.

Here's a Working Sample StackBlitz for your ref.

Angular 2 change detection - refreshing DOM even though nothing has changed

I found the answer to my own question. My problem was using arrays that are generated by a get method in my template. Aka. there are 2 models: [ParentModel, ChildModel], the ParentModel holds an array of ChildModels. Now I wanted to get only a selection of those ChildModels from the ParentModel and what I did was

class ParentModel {
__specificChildren() : IChild[] {}
}

and in the template, I used

*ngFor="let child of parentModel.__specificChildren()"

This started refreshing endlessly. Probably because some global events triggered the change detection on the related Models, which created an array with a different pointer each time it was checked, which told Angular that something had changed and thus triggered change detection.

Anyway, how I solved this is, instead of having the function in the template, which was optimal, because I did not want to create an useless variable in the component to complicate things, I still created the helper variable in the component, thus resulting in:

Component

public specificChildren: IChild[]

parentDatatabe.subscribe( _parent {
this.parent = _parent;
this.specificChildren = _parent.__specificChildren()
})

Template

*ngFor="let child of specificChildren"

Thus only setting the new children, when the parent database pushes a new value.



Related Topics



Leave a reply



Submit