How to use trackBy with ngFor in Angular

Using trackBy with ngFor in Angular dramatically improves list rendering performance by tracking items by unique identifier instead of object reference. As the creator of CoreUI with over 12 years of Angular experience since 2014, I’ve optimized countless lists with trackBy in enterprise applications. Angular’s trackBy function prevents unnecessary DOM manipulation when list items change, reducing re-renders and improving application responsiveness. This approach is essential for lists that update frequently or contain complex templates.

Implement trackBy function with ngFor to track list items by unique identifier and prevent unnecessary re-renders.

Basic trackBy usage:

// user-list.component.ts
import { Component } from '@angular/core'

interface User {
  id: number
  name: string
  email: string
}

@Component({
  selector: 'app-user-list',
  template: `
    <div class="user-list">
      <div *ngFor="let user of users; trackBy: trackByUserId" class="user-card">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
      </div>
    </div>
  `
})
export class UserListComponent {
  users: User[] = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Smith', email: '[email protected]' },
    { id: 3, name: 'Bob Johnson', email: '[email protected]' }
  ]

  trackByUserId(index: number, user: User): number {
    return user.id
  }

  refreshUsers(): void {
    // Fetch new data - items with same ID won't re-render
    this.users = [
      { id: 1, name: 'John Doe Updated', email: '[email protected]' },
      { id: 2, name: 'Jane Smith', email: '[email protected]' },
      { id: 4, name: 'New User', email: '[email protected]' }
    ]
  }
}

Performance comparison:

// without-trackby.component.ts
@Component({
  selector: 'app-without-trackby',
  template: `
    <div *ngFor="let item of items" class="item">
      {{ item.name }}
    </div>
  `
})
export class WithoutTrackByComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]

  updateItems(): void {
    // Without trackBy, ALL DOM elements are recreated
    this.items = [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ]
  }
}

// with-trackby.component.ts
@Component({
  selector: 'app-with-trackby',
  template: `
    <div *ngFor="let item of items; trackBy: trackById" class="item">
      {{ item.name }}
    </div>
  `
})
export class WithTrackByComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]

  trackById(index: number, item: any): number {
    return item.id
  }

  updateItems(): void {
    // With trackBy, only changed items are updated
    this.items = [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ]
  }
}

Advanced trackBy patterns:

// product-list.component.ts
import { Component, OnInit } from '@angular/core'

interface Product {
  id: string
  name: string
  price: number
  category: string
}

@Component({
  selector: 'app-product-list',
  template: `
    <div class="products">
      <div *ngFor="let product of products; trackBy: trackByProductId; let i = index"
           class="product-card">
        <span class="index">{{ i + 1 }}</span>
        <h3>{{ product.name }}</h3>
        <p>{{ product.price | currency }}</p>
        <span class="category">{{ product.category }}</span>
      </div>
    </div>
  `
})
export class ProductListComponent implements OnInit {
  products: Product[] = []

  ngOnInit(): void {
    this.loadProducts()
  }

  trackByProductId(index: number, product: Product): string {
    return product.id
  }

  loadProducts(): void {
    this.products = [
      { id: 'p1', name: 'Laptop', price: 999, category: 'Electronics' },
      { id: 'p2', name: 'Mouse', price: 29, category: 'Accessories' },
      { id: 'p3', name: 'Keyboard', price: 79, category: 'Accessories' }
    ]
  }

  sortByPrice(): void {
    // Items maintain DOM elements when sorted
    this.products = [...this.products].sort((a, b) => a.price - b.price)
  }

  filterByCategory(category: string): void {
    // Only removed items have DOM elements destroyed
    this.products = this.products.filter(p => p.category === category)
  }
}

trackBy with complex objects:

// order-list.component.ts
interface Order {
  orderId: string
  customer: {
    id: number
    name: string
  }
  items: Array<{ productId: string; quantity: number }>
  total: number
}

@Component({
  selector: 'app-order-list',
  template: `
    <div *ngFor="let order of orders; trackBy: trackByOrderId" class="order">
      <h3>Order #{{ order.orderId }}</h3>
      <p>Customer: {{ order.customer.name }}</p>
      <div *ngFor="let item of order.items; trackBy: trackByProductId" class="item">
        Product: {{ item.productId }} - Qty: {{ item.quantity }}
      </div>
      <p>Total: {{ order.total | currency }}</p>
    </div>
  `
})
export class OrderListComponent {
  orders: Order[] = []

  trackByOrderId(index: number, order: Order): string {
    return order.orderId
  }

  trackByProductId(index: number, item: any): string {
    return item.productId
  }
}

Best Practice Note

Always use trackBy with ngFor for lists that change frequently. Track by unique identifier (id), not by index, for stable tracking. Use simple primitive values (number, string) as tracking keys for best performance. Avoid complex calculations in trackBy function—it runs frequently. TrackBy prevents Angular from destroying and recreating DOM elements unnecessarily. This is how we optimize lists in CoreUI Angular components—trackBy for all dynamic lists, reducing re-renders by up to 90% and significantly improving application performance.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author