trackBy List Performance
Understand how Angular tracks list items, reuses existing DOM nodes, preserves UI state, and improves rendering performance using the @for track expression and the traditional trackBy function.
Lesson content
What is List Tracking? When Angular renders a list, it must connect every data item with a corresponding DOM element. List tracking gives Angular a stable identity for each item so it can reuse existing DOM nodes instead of unnecessarily destroying and recreating them. Simple Definition List Tracking = Data Item Identity + DOM Node Reuse + Minimum DOM Updates List Rendering Flow Data Collection ↓ Angular Reads Tracking Key ↓ Matches Items with Existing Views ↓ Reuses, Moves, Creates, or Removes Required Views ↓ Updated List Displayed Why is List Tracking Important? Reduces unnecessary DOM creation Improves rendering performance Preserves input values and cursor positions Preserves focus and text selection Supports correct list animations Handles adding, removing, and reordering efficiently Prevents child components from being recreated unnecessarily Modern List Rendering with @for Modern Angular templates use the @for block to render collections. The track expression identifies every item in the collection. import { Component } from '@angular/core'; interface User { id: number; name: string; } @Component({ selector: 'app-user-list', template: ` @for ( user of users; track user.id ) { <p> {{ user.name }} </p> } ` }) export class UserListComponent { users: User[] = [ { id: 1, name: 'Ajay' }, { id: 2, name: 'Sneha' } ]; } Tracking Flow user.id ↓ Unique Tracking Key ↓ Angular Connects User with Existing DOM Node ↓ Only Changed Items Update Why Use a Unique ID? A unique and stable identifier allows Angular to recognize the same item even when: The array receives a new reference Items are reordered An item is updated New items are inserted Existing items are removed @for ( product of products; track product.id ) { <app-product-card [product]="product" /> } Common tracking properties include: item.id item.uuid item.code item.key item.databaseId Without Stable Tracking Consider a list returned from an API. users = [ { id: 1, name: 'Ajay' }, { id: 2, name: 'Sneha' } ]; After refreshing, the API may return new object instances: users = [ { id: 1, name: 'Ajay' }, { id: 2, name: 'Sneha' } ]; The data looks the same, but the object references are new. Tracking by user.id tells Angular that these are still the same logical users. Without Stable Identity Old Object Reference ≠ New Object Reference ↓ Angular May Perform More DOM Work With Stable Identity Old User ID: 1 = New User ID: 1 ↓ Reuse Existing View Adding an Item addUser() { this.users = [ ...this.users, { id: 3, name: 'Rahul' } ]; } With track user.id , Angular keeps the existing user views and creates only the new view. Existing ID 1 → Reuse Existing ID 2 → Reuse New ID 3 → Create Removing an Item removeUser(id: number) { this.users = this.users.filter( user => user.id !== id ); } Angular removes only the DOM view connected with the deleted identifier. ID 1 → Reuse ID 2 → Remove ID 3 → Reuse Updating an Item renameUser( id: number, name: string ) { this.users = this.users.map( user => user.id === id ? { ...user, name } : user ); } The tracking key remains the same, so Angular keeps the existing view and updates its bindings. Same ID ↓ Same List Item ↓ Reuse View ↓ Update Displayed Name Reordering a List reverseUsers() { this.users = [ ...this.users ].reverse(); } With stable tracking keys, Angular can move existing DOM nodes into their new positions. Before: ID 1 ID 2 ID 3 After: ID 3 ID 2 ID 1 Stable IDs ↓ Move Existing Views ↓ Do Not Recreate All Views Tracking by $index The loop index can be used as the tracking key. @for ( country of countries; track $index ) { <p> {{ country }} </p> } Tracking by $index is suitable mainly for static collections whose order and contents do not change. Good Use Case readonly weekdays = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday' ]; @for ( day of weekdays; track $index ) { <p>{{ day }}</p> } Avoid $index When Items can be inserted at the beginning or middle Items can be deleted Items can be reordered Rows contain editable fields Rows contain stateful child components Why $index Can Cause Problems Suppose the list is: Index 0 → Ajay Index 1 → Sneha Index 2 → Rahul A new user is inserted at the beginning: Index 0 → Neha Index 1 → Ajay Index 2 → Sneha Index 3 → Rahul Every existing item's index changes. Angular may associate an existing DOM view with the wrong logical item because the index represents position, not item identity. Index = Position Unique ID = Item Identity Tracking the Item Reference Angular also allows the item itself to be used as the tracking value. @for ( user of users; track user ) { <p> {{ user.name }} </p> } This tracks items using their object references. It may work when the same object instances are preserved, but it performs poorly when API refreshes or immutable updates create new objects. Reference Tracking Problem Old User Object ↓ API Refresh ↓ New User Object with Same ID ↓ Reference Is Different Prefer a unique property such as user.id whenever possible. Tracking with a Function A function can be called from the track expression when the identity requires custom logic. import { Component } from '@angular/core'; interface Order { storeId: number; orderNumber: string; } @Component({ selector: 'app-order-list', template: ` @for ( order of orders; track orderKey(order) ) { <p> {{ order.orderNumber }} </p> } ` }) export class OrderListComponent { orders: Order[] = []; protected orderKey( order: Order ): string { return ( `${order.storeId}-` + order.orderNumber ); } } Important Syntax Correct: track orderKey(order) Incorrect: track orderKey The function must be invoked so Angular receives the returned tracking key rather than the function reference itself. Tracking Function Requirements A tracking function should: Return a unique key for every unique item Return the same key for the same logical item Be fast Have no side effects Not modify the collection Not generate a random value Good Tracking Function protected trackOrder( order: Order ): string { return order.orderNumber; } Bad Tracking Function protected trackOrder( order: Order ): number { return Math.random(); } A random key changes every time, making Angular treat every item as a new item. Duplicate Tracking Keys Every item in the current collection must have a unique tracking key. Incorrect Example products = [ { id: 1, category: 'electronics' }, { id: 2, category: 'electronics' } ]; @for ( product of products; track product.category ) { <p>{{ product.id }}</p> } Both products produce the same key: electronics electronics Angular cannot reliably determine which DOM node belongs to which product. Correct Example @for ( product of products; track product.id ) { <p>{{ product.id }}</p> } Stable Tracking Keys A tracking key must remain stable during the lifetime of an item. Avoid Editable Properties @for ( user of users; track user.name ) { <input [value]="user.name" /> } If the user's name changes, Angular sees a different key and may recreate the view. Prefer Immutable Identity @for ( user of users; track user.id ) { <input [value]="user.name" /> } user.id = Stable Identity user.name = Editable Data List Inputs and UI State Stable tracking is especially important when list rows contain form fields. @for ( user of users; track user.id ) { <input [value]="user.name" /> } When Angular reuses the correct DOM node, it can preserve browser-managed state such as: Input focus Cursor position Text selection Current option selection Element animation state Child Component Performance Child Component import { Component, input, OnDestroy, OnInit } from '@angular/core'; @Component({ selector: 'app-user-row', template: ` <p>{{ user().name }}</p> ` }) export class UserRowComponent implements OnInit, OnDestroy { user = input.required<User>(); ngOnInit() { console.log( 'Row created' ); } ngOnDestroy() { console.log( 'Row destroyed' ); } } Parent List @for ( user of users; track user.id ) { <app-user-row [user]="user" /> } Stable tracking prevents unaffected child-component instances from being destroyed and recreated. List Updated ↓ Compare IDs ↓ Unchanged ID ↓ Reuse Child Component Tracking and Immutable Updates Immutable updates create new array or object references. this.users = this.users.map( user => user.id === updatedUser.id ? updatedUser : user ); This pattern works efficiently with track user.id . New Array Reference ↓ Some New Object References ↓ Stable IDs Remain ↓ Angular Reuses Existing Views Filtering and Sorting Filtered List visibleUsers = computed(() => this.users().filter( user => user.active ) ); @for ( user of visibleUsers(); track user.id ) { <p>{{ user.name }}</p> } Sorted List sortedUsers = computed(() => [...this.users()].sort( ( first, second ) => first.name.localeCompare( second.name ) ) ); @for ( user of sortedUsers(); track user.id ) { <p>{{ user.name }}</p> } Tracking by ID allows Angular to move existing views when sorting changes the order. Pagination @for ( product of currentPage(); track product.id ) { <app-product-card [product]="product" /> } Tracking improves page updates, but it does not keep a view alive after its item is removed from the rendered collection. When moving to a completely different page, Angular removes the old page views and creates views for the new page items. Large List Performance Stable tracking reduces DOM work, but it does not make rendering thousands of visible elements inexpensive. For large lists, combine tracking with: Pagination Virtual scrolling Lazy loading OnPush change detection Signals or memoized derived state Efficient child components Server-side filtering and sorting Performance Formula Large List Performance = Stable Tracking + Fewer Visible Rows + Efficient Components + Efficient Data Processing Traditional *ngFor trackBy Existing Angular applications may use *ngFor with a trackBy function. import { TrackByFunction } from '@angular/core'; trackByUserId: TrackByFunction<User> = ( index, user ) => user.id; Template <div *ngFor=" let user of users; trackBy: trackByUserId " > {{ user.name }} </div> For new templates, prefer the @for block and its required track expression. @for track vs *ngFor trackBy @for track *ngFor trackBy Modern built-in control flow Traditional structural directive Uses a tracking expression Uses a TrackByFunction track user.id trackBy: trackByUserId No directive import required Requires NgFor or CommonModule Preferred for new templates Common in existing code Using Context Variables The @for block exposes contextual variables. Variable Meaning $index Current item index $count Total number of items $first Whether the item is first $last Whether the item is last $even Whether the index is even $odd Whether the index is odd Example @for ( user of users; track user.id; let position = $index; let firstItem = $first ) { <p [class.first]="firstItem" > {{ position + 1 }}. {{ user.name }} </p> } The displayed index can change while the stable tracking key remains the same. $index = Current Position user.id = Stable Identity Empty List Handling The @empty block displays content when the collection has no items. @for ( user of users; track user.id ) { <p> {{ user.name }} </p> } @empty { <p> No users found. </p> } Complete Performance Example import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; interface Product { id: number; name: string; active: boolean; } @Component({ selector: 'app-product-list', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <button (click)="addProduct()" > Add Product </button> <button (click)="reverseProducts()" > Reverse </button> @for ( product of activeProducts(); track product.id; let index = $index ) { <article> {{ index + 1 }}. {{ product.name }} <button (click)="removeProduct( product.id )" > Remove </button> </article> } @empty { <p> No active products. </p> } ` }) export class ProductListComponent { private nextId = 3; products = signal<Product[]>([ { id: 1, name: 'Keyboard', active: true }, { id: 2, name: 'Mouse', active: true } ]); activeProducts = computed(() => this.products().filter( product => product.active ) ); addProduct() { const id = this.nextId++; this.products.update( products => [ ...products, { id, name: `Product ${id}`, active: true } ] ); } removeProduct(id: number) { this.products.update( products => products.filter( product => product.id !== id ) ); } reverseProducts() { this.products.update( products => [...products].reverse() ); } } How to Measure List Performance Do not optimize based only on assumptions. Use browser development tools and Angular profiling tools to inspect: Component creation and destruction Rendering time Layout and paint work Long-running JavaScript tasks DOM node count Repeated API or computed operations Simple Debugging Technique Add lifecycle logging temporarily to a row component. ngOnInit() { console.log( 'Created:', this.user().id ); } ngOnDestroy() { console.log( 'Destroyed:', this.user().id ); } After updating or reordering the list, unaffected rows should not be destroyed and recreated. Common Mistakes 1. Tracking by a Non-Unique Property Avoid: track user.role Many users may have the same role. Prefer: track user.id 2. Tracking by an Editable Property Avoid: track user.name Prefer: track user.id 3. Tracking by $index for Dynamic Lists The index represents the current position and can change after insertion, deletion, filtering, or sorting. 4. Generating Random Keys Avoid: track randomKey() A changing key forces Angular to treat items as new. 5. Returning Objects Created During Tracking Avoid: track { id: user.id } This creates a new object reference during evaluation. Prefer: track user.id 6. Forgetting to Call a Tracking Function Avoid: track trackUser Prefer: track trackUser(user) 7. Performing Expensive Work in the Tracking Function Tracking logic runs during list reconciliation and should remain very fast. 8. Assuming Tracking Solves Every Performance Problem Tracking reduces DOM changes but does not reduce the number of visible elements or expensive work performed inside every row. Best Practices Use the @for block for new Angular templates Track items using a unique and stable identifier Prefer track item.id when an ID is available Use $index only for static collections Avoid tracking mutable display properties Ensure tracking keys are unique within the collection Keep tracking functions fast, stable, and side-effect free Use immutable collection updates Combine tracking with OnPush and efficient state management Use pagination or virtual scrolling for very large lists Measure actual performance before introducing complex optimizations Interview Questions What is trackBy? trackBy is the traditional *ngFor mechanism for returning a stable identity for every list item so Angular can reuse the correct DOM views. What is the modern replacement for trackBy? Modern @for blocks use a required track expression such as track user.id . Why is tracking important? It allows Angular to perform the minimum necessary DOM operations when items are added, removed, updated, or reordered. What should a tracking expression return? It should return a unique, stable, and quickly calculated value for each logical item. Why is an ID better than the index? An ID represents the item's identity, while the index represents only its current position. When is tracking by $index acceptable? It is suitable for static collections whose items and ordering never change. What happens with duplicate tracking keys? Angular cannot reliably connect data items with their correct DOM views, which can cause incorrect updates and slower reconciliation. Can a tracking function have side effects? No. It should be idempotent, fast, and side-effect free. Does trackBy prevent all list rendering? No. Angular still creates views for new items, removes views for deleted items, and updates bindings for changed items. Tracking prevents unnecessary recreation of unaffected views. Does tracking replace virtual scrolling? No. Tracking reduces DOM updates, while virtual scrolling reduces the number of DOM elements rendered at one time. Key Point A stable tracking key connects each logical data item with its existing DOM view, allowing Angular to reuse views and perform only the required updates. Easy Formula Modern List = @for + track item.id Traditional List = *ngFor + trackBy Function Good Tracking Key = Unique + Stable + Fast + Side-Effect Free Dynamic List = Track by ID Static List = Track by $index Large List = Tracking + Pagination or Virtual Scrolling + Efficient Row Components Collection Changes ↓ Compare Tracking Keys ↓ Same Key = Reuse or Move View New Key = Create View Missing Key = Remove View