Toolbox
Shelf

Pages

  • Home
  • Shelf
  • Toolbox

Extras

  • Resume

Crafted with and few cups of coffee.

Designed in Figma • Built with Next.js & Tailwind • Hosted on Vercel

© 2026 Gentle Joseph | All rights reserved.

May 29th, 2022•4 min read

Improve *ngFor performance with trackBy

Fewer computations -> performance gain! 🥳

As we all know, in terms of performance, DOM manipulations are rather expensive than regular JavaScript code.

Angular’s ngFor directive is highly optimized in a way that reduces DOM manipulation to a bare minimum. So, if an element is added or removed from an array, the whole list isn’t getting re-rendered. Instead, it reuses all existing DOM elements and only creates or removes the new element.

Similarly, when the element changes its position in the array, only that change in position of DOM element gets noticed.

Angular determines all the DOM manipulation and does all optimisation since it conducts identification of each object in the array. It uses the reference of the object for that.

Note: Angular 17+ introduced the @for block syntax with a mandatory track expression, which replaces *ngFor with trackBy. The concepts below still apply, but the modern syntax is @for (item of items; track item.id) { ... }.

Why & when to use trackBy then?

Unfortunately, Angular’s default method of identifying objects by their reference is quite constrained especially in circumstances where a reference shift can’t be prevented.
 

If we are working with REST API or any type of immutable data structure, the reference of each object will be kept on changing. This forces Angular to give up its default optimisations and forces it to re-render the whole data set. This is because every reference is modified and therefore every object seems new to Angular.

As you can probably tell this heavily impacts the performance. Especially if your collection is huge. To avoid this, we can help Angular to identify each object in the collection by having a trackBy attribute. A trackBy attribute allows us to define a function that returns a unique identifier for each iterable element. This helps bypass unnecessary and expensive change detection when the data set changes, say for example on receiving new data from an API call.

How to use trackBy?

We need to create a function within our component that matches TrackByFunction interface. The trackBy function takes the index and the current element as arguments and it returns the unique identifier for this element.

interface TrackByFunction<T> {
  (index: number, item: T): any;
}

// component level function should look something like this
function trackByTransaction(index: number, item: Transaction): string {
  return item.id;
}

And use it in the template like this:

<div *ngFor="let item of txnList; trackBy: trackByTransaction">
  <!-- do stuff -->
</div>

The modern way: @for with track (Angular 17+)

If you're on Angular 17 or later (including Angular 20), the *ngFor directive is still supported but the recommended approach is the new @for block syntax. The big difference is that track is now mandatory - Angular won't let you forget it.

The syntax is much cleaner. Instead of creating a separate trackBy function in your component, you write the tracking expression right in the template:

@for (item of txnList; track item.id) {
  <div>
    <!-- do stuff -->
  </div>
} @empty {
  <div>No transactions found</div>
}

That's it. No TrackByFunction interface, no separate component method. Just track item.id inline.

You also get the @empty block for free, which renders when the list is empty. Previously you'd need a separate *ngIf for that.

Why is track mandatory now?

The Angular team made tracking mandatory because skipping it was one of the most common performance mistakes. With *ngFor, developers would forget to add trackBy and only notice the problem when lists got large and the app started lagging. By making it required, Angular forces you to think about it upfront.

Migration from *ngFor

If you're migrating an existing app, Angular provides a schematic to convert your templates automatically:

ng generate @angular/core:control-flow

This will convert your *ngFor directives to @for blocks across your project. It handles the trackBy function conversion too - if you had a trackBy function, it extracts the tracked property and puts it inline.

Quick comparison

<!-- Old way (*ngFor) -->
<div *ngFor="let item of txnList; trackBy: trackByTransaction">
  {{ item.name }}
</div>

<!-- New way (@for) -->
@for (item of txnList; track item.id) {
  <div>{{ item.name }}</div>
}

The concepts haven't changed - Angular still uses the tracked value to decide which DOM elements to reuse, create, or destroy. The syntax is just better now.

Back to Category