Issue
I was getting bad performance on an Angular Material table. When I checked the console I could see it was binding each row multiple times although I can’t see what’s triggering it. I have reduced my code as simple as I can and I am still seeing the issue.
There are 257 rows, but the count in being logged to the console counts to 1542, which means each row seems to be being bound 6 times. The formatting function decimalTimeToHours should be called once for each row. Any ideas why it’s binding so many times?
import {HttpClient} from '@angular/common/http';
import {EndPoints} from '../utils/endPoints';
import {MarketMakerEntry} from '../types/MarketMakerEntry';
import {MatTableDataSource} from '@angular/material/table';
@Component({
selector: 'app-market-maker',
templateUrl: './market-maker.component.html',
styleUrls: ['./market-maker.component.css']
})
export class MarketMakerComponent {
count = 0;
dataSource: MatTableDataSource<MarketMakerEntry>;
title = 'test';
displayedColumns: string[] = ['EstimatedTimeToBidFill'];
constructor(private http: HttpClient) {
http.get<MarketMakerEntry[]>(EndPoints.GET_MARKETS).subscribe((marketMakerEntries: MarketMakerEntry[]) => {
console.log(marketMakerEntries.length);
this.dataSource = new MatTableDataSource<MarketMakerEntry>(marketMakerEntries);
});
}
decimalTimeToHours(value: number): string {
console.log(this.count++);
return String(value);
}
}
<div class="mainContainer">
<table mat-table [dataSource]="dataSource" class="bidAskTable">
<ng-container matColumnDef="EstimatedTimeToBidFill">
<th mat-header-cell *matHeaderCellDef>ET Bid Fill</th>
<td mat-cell *matCellDef="let element"> {{decimalTimeToHours(element.EstimatedTimeToBidFill)}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; let i = index; columns: displayedColumns;" [ngClass]="{'redRow': row.NetGain <= 0, 'greenRow': row.NetGain > 0}"></tr>
</table>
</div>
</div>
Solution
The reason your decimalTimeToHours
method is getting called so many times relates to how Angular change detection works. Angular keeps track of any bindings in the HTML template, and updates the browser dom when the underlying value changes. But, this isn’t a magical process. In order to know when an underlying value changes, Angular has to manually keep checking if these values have changed. It takes the current value and compares it against the last value it saw for that binding. If the value changes, it updates the dom. The finer details of how and when this detection process runs is a huge topic. The important part, though, is that when change detection runs (which appears to be happening 6 times) it goes to your binding to get the current value.
The problem stems from your use of a function there right in the template. Each time change detection wants to evaluate the value, it has to call that function in order to determine the value (which creates a new string each time), and then compares that against the old string. Not a huge problem, but it’s something you could easily be avoiding. One solution would be to use an angular pipe to format the data the way you want to see it in the table:
<td mat-cell *matCellDef="let element"> {{element.EstimatedTimeToBidFill | decimalTimeToHoursPipe}}</td>
Angular is smart here and knows to only call the pipe function when the value has changed. You might not even need to create a custom pipe. Check out the ones that come with Angular: Pipes
Another question you might have is "can I reduce the number of times change detection runs?". And the answer is yes. You can look into setting the ChangeDetectionStrategy
for your component to OnPush
. Some info here. Also, know that Angular runs extra cycles of change detection in development mode to check if you’re doing something bad in terms of assigning variables referenced by the templates at inopportune times. But, when you do a prod build for deployment, that no longer happens, and your app gets a mild performance boost.