In a recent presentation, I had an individual very focused on the need for trackBy
on my ngFor
... to the exclusion of everything else in the talk. He even went as far as to push a pull-request on my talk repository to add the change.
I was perplexed.
- I know there used to be a need for
trackBy
. - Experience has shown me little need for it's use recently.
- Some quick research said that it is only needed if there are issues with performance with modern Angular.
So, I decided to test this out myself.
The First Iteration
In my first iteration of testing, I tested loading data to see if there was a difference in load times when displaying them with and without the trackBy
. Here are the first set of tests I ran that showed some hopeful results.
Data
Basically, the data was 100,000 records with:
- An integer, 0-based
index
. - An identifier that is a random 50 characters A-Z, a-z, or 0-9.
Summary of First Iteration
From this point, I refreshed the page 25-times each, noting the time to finish on the Network tab.
Type | WITH | WITHOUT |
---|---|---|
data 01 | 6.17 | 6.77 |
data 02 | 6.14 | 6.29 |
data 03 | 6.31 | 6.28 |
data 04 | 6.34 | 6.33 |
data 05 | 6.23 | 6.06 |
data 06 | 6.14 | 6.31 |
data 07 | 6.14 | 6.21 |
data 08 | 6.46 | 6.22 |
data 09 | 6.29 | 6.09 |
data 10 | 6.38 | 6.37 |
data 11 | 6.22 | 6.22 |
data 12 | 6.38 | 6.43 |
data 13 | 6.23 | 6.19 |
data 14 | 6.22 | 6.15 |
data 15 | 6.38 | 6.33 |
data 16 | 6.16 | 6.45 |
data 17 | 6.32 | 6.19 |
data 18 | 6.21 | 6.18 |
data 19 | 6.25 | 6.36 |
data 20 | 6.16 | 6.17 |
data 21 | 6.46 | 6.03 |
data 22 | 6.22 | 6.12 |
data 23 | 6.30 | 6.44 |
data 24 | 6.23 | 6.67 |
data 25 | 6.20 | 5.98 |
RESULTS | 6.26 | 6.27 |
Conclusion
The results were one-hundredth of a second different, my conclusion might be that trackBy
would only be needed if there were some significant performance need.
This path only tested Angular's ability to place data on-screen. I realized that the trackBy
was built as a means to allow for faster screen updates when the data changed.
So, on to the second iteration ...
Second Iteration
I decided to go with a smaller data-set and initialize a series of changes where I swapped two indexes. I opted for a 500ms delay in between loading the data and initiating the swaps.
The (Real) Work
The repository is HERE.
Basically, the data is now 10,000 records with:
- An integer, 0-based
index
. - An identifier that is a random 50 characters A-Z, a-z, or 0-9.
Each page connects to the service, gets the data and displays the identifier 10,000 times. On one page, we are using trackBy
and in the other, we are not using it.
Additionally, after 500ms another process triggers that randomly picks two indexes and swaps them. I did this 1,000 times per page.
data.service.ts
The core to the service is as follows ...
iterations: number = 1000;
dataPoints: number = 100000;
data: Array<any> = [];
startTime: Date;
constructor() {
this.startTime = new Date();
this.init();
}
init = (): void => {
for (let i = 0, len = this.dataPoints; i < len; i++) {
const datum: any = { index: i, identifier: this.makeid() };
this.data.push(datum);
}
};
makeid = (length: number = 50): string => {
var result: string = '';
var characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
getRandomInt = (min: number, max: number) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
with.component.ts
The components are nearly identical ...
data: Array<any> = [];
constructor(
private dataService: DataService
) { }
ngOnInit(): void {
this.data = this.dataService.data;
setTimeout(this.initiateReorganize.bind(this), 500);
}
ngAfterViewChecked(): void {
const now: Date = new Date();
const difference: number = now.getTime() - this.dataService.startTime.getTime();
console.log('difference: ', difference);
}
identify = (index: number, item: any): string => item.index;
initiateReorganize = (): void => {
const min: number = 0;
const max: number = this.dataService.iterations - 1;
for (let i = 0, len = this.dataService.iterations; i < len; i++) {
const a: number = this.dataService.getRandomInt(min, max);
const b: number = this.dataService.getRandomInt(min, max);
[this.data[a], this.data[b]] = [this.data[b], this.data[a]];
}
};
... except, the without.component.ts
does not contain an identify
function.
with.component.html
And, the HTML is nearly identical, as well ...
<div *ngFor="let item of data; trackBy: identify">
{{ item.identifier }}
</div>
... removing the trackBy: identify
in the without.component.html
, as well.
Results
From this point, I refreshed the page 25-times each, noting the time to finish in the console.
Type | WITH | WITHOUT |
---|---|---|
Data 01 | 1700 | 1654 |
Data 02 | 1647 | 1669 |
Data 03 | 1634 | 1695 |
Data 04 | 1639 | 1652 |
Data 05 | 1753 | 1641 |
Data 06 | 1624 | 1693 |
Data 07 | 1627 | 1632 |
Data 08 | 1676 | 1637 |
Data 09 | 1638 | 1707 |
Data 10 | 1631 | 1630 |
Data 11 | 1625 | 1652 |
Data 12 | 1727 | 1648 |
Data 13 | 1633 | 1768 |
Data 14 | 1636 | 1641 |
Data 15 | 1684 | 1712 |
Data 16 | 1634 | 1667 |
Data 17 | 1690 | 1633 |
Data 18 | 1631 | 1730 |
Data 19 | 1624 | 1631 |
Data 20 | 1741 | 1640 |
Data 21 | 1635 | 1675 |
Data 22 | 1631 | 1641 |
Data 23 | 1690 | 1663 |
Data 24 | 1625 | 1652 |
Data 25 | 1688 | 1651 |
Results | 1658.52 | 1664.56 |
Conclusion
So, given that the results are 6.04 milliseconds different (remember, less than a second difference on load of a data-set 10-times larger), my conclusion would be that trackBy
would only be needed if there were some significant performance need that cropped up.
In general, it is not needed in modern Angular.
Top comments (2)
It's a totally wrong experiment:\ "trackBy" prevents application from excessive DOM re-rendering. The load of the application - this is not the case.
That actually makes total sense and one shouldn't just use
trackBy
religiously (as any other recommended "best practice"), but rather consider each iterable use case separately.There are use cases though where absence of
trackBy
can cause huge performance drops. For instance when one iterates over component instances that have some costly calculations inside (triggered in ngOnInit, for instance). If these calculations don't need to be triggered if nothing changes for a specific item,trackBy
will save a ton of potentially lost milliseconds.So salt is the key, as always - you should always take everything with a grain of it.