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:
Angular: Prevent DomSanizer from updating on DOM Events
Angular performance: ngStyle recalculates on each click on random input
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.
- In String Interpolation -
{{ methodCall() }}
- In Property Binding -
[propertyName]="methodCall()"
- 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:
- DOM Events.
- AJAX Calls.
- 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 Observable
s 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 interface
s 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 FormArray
s 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 FormArray
s. 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:
And here's the summary with the refactored implementation:
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
Number Input - Always Show Spin Buttons
Advantages and Disadvantages of Using Base64 Encoded Images
Height Is Not Correct in Flexbox Items in Chrome
Box-Shadow on Ie9 Doesn't Render Using Correct CSS, Works on Firefox, Chrome
Angular Checkboxes "Select All" Functionality with Only One Box Selected Initially
Multiple Fonts in Font-Family Property
Why Use a Sprite Sheet Rather Than Individual Images
How to Detect Only with CSS Mobile Screens
Bootstrap 4 Order Masonry-Like Column Cards Horizontal Instead of Vertical
Bootstrap 4: Center Inline Form Inside a Jumbotron
Img's Max-Height Not Respecting Parent's Dimensions
What Are The Default Values for Justify-Content & Align Content
Is There a Jdk Class to Do HTML Encoding (But Not Url Encoding)
Twitter Bootstrap: Column Re-Ordering for Full Width Divs
How to Format Code in HTML/CSS/Js/ PHP
Missalignment with Inline-Block (Other Elements Pushed Down)