DEV Community

Eduard Krivanek for This is Angular

Posted on

Angular - For Loop Optimizations

In my recent article about Senior Angular Interview Questions, I mentioned a section about optimizing forloops with the trackmethod. However, I also pointed out that the choice of value you use for tracking really matters. Even if you add track, it might not actually make your rendering more performant if used incorrectly. This article expands on that section with additional explanation and examples.

The track function is a useful performance optimization that was often overlooked with the old *ngFor="let item of items" syntax. Fortunately, the new control flow @for() now requires you to specify a track function, which encourages better practices.

So, why is it important? Imagine you have a component that makes an API call to fetch a list of items, list of users, and displays them in the template. You also have a “reload” button to refetch this data (in case something has changed on the backend). Below is an example using the older *ngFor syntax to illustrate the issue:

@Component({
  selector: 'app-child',
  imports: [NgForOf],
  template: ` 
    <button (click)="onRerun()">re run</button>

    <div *ngFor="let item of items()">
        {{item.name}}
    </div>
`
})
export class ChildComponent {
  items = signal<{ id: string; name: string }[]>([]);

  onRerun() {
    // "fake api call" to reload data
    this.items.set([{id: '100', name: 'Item 1'}, /* ... */ ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this setup, every time onRerun() is triggered and the array is updated (even with the same content), Angular will re-render all elements in the DOM. That's because it can't tell which items stayed the same and why didn't. It result to performance loss and UI flickering, especially in long or complex lists. To prevent this, you use a trackBy function:

@Component({
  selector: 'app-child',
  imports: [CommonModule],
  template: ` 
    <ng-container *ngFor="let item of items(); trackBy: identify">
        <!-- previous code -->
    </ng-container>
`
})
export class ChildComponent {
  // ... previous code

  identify(index: number, item: { id: string }): string | number {
    return item.id;
  }
}
Enter fullscreen mode Exit fullscreen mode

This tells Angular how to uniquely identify items in the array, commonly via an id. With a trackBy function (or track key in @for()), Angular can associate each item with its corresponding DOM element. When the data is reloaded, Angular compares these keys (not full object references), allowing unchanged items to be preserved in the DOM.

Why does this matter? Because DOM operations are expensive. Without proper tracking, Angular discards and recreates DOM elements for every item, even if the data hasn't changed. With tracking, the DOM elements stay in place, and Angular only updates bindings when necessary.

On the GIF below, the top list uses trackBy: identify while the second one does not. You can see the difference. The top list preserves DOM elements during data reload, whereas the second recreates them entirely each time.

NgFor Rerendering Whole List

With the new @for() syntax, Angular enforces the use of a track key for the same purpose. However, two common mistakes still happen:

  • Using the object itself as the key - example: @for (item of items(); track item). This does not work as expected because the reference to each item changes on every reload, even if the data is identical and it will re-render the UI every time, basically ignoring the track function.
  • Using $index as the key - example: @for (item of items(); track $index). This causes problems when an item is removed. Suppose you delete the 5th item in a list of 10, then every item after index 4 now has a new index, forcing Angular to re-render them all unnecessarily. In stateful components like forms, this leads to loss of input focus or cursor position, however using the $index is okay for static lists.

Here's a comparison: the top row uses track item.id, and the second uses track $index. Watch how the first preserves DOM elements during removal. Here is a stackblitz example to play with.

NgFor Using Index

If you’re rendering a static list of rows in a table, or a fixed ordered list that never changes its order or size, tracking by $index works because the identity of each item will never shift. In these scenarios, you don’t risk losing focus, cursor position, or component state since nothing is being reordered or removed. Use $index only for completely static, non-interactive lists. For anything dynamic or stateful, always track by a stable identifier like id.

A great example where proper tracking really matters is an infinite scroll or chat application. Imagine you’re building a chat window. As new messages arrive, they’re pushed into the list, and when you scroll up, older messages are loaded from the server.

If you don’t use a proper track key (like message.id), Angular will re-render the entire list whenever new data is appended. That means the scroll position jumps, the user loses their place, and any ongoing animations are lost.

Using an unique key for tracking (like ID), Angular reuses the existing DOM nodes for older messages and only creates DOM nodes for the new ones. This preserves scroll position, keeps animations smooth, and makes the whole app feel more stable and responsive. On the following example you see an incorrect usage of for loop, using the $index for tracking and every time a new message comes, the whole list is re-rendered.

@Component({
  selector: 'app-chat',
  standalone: true,
  template: `
     @for(msg of messages(); track $index){
         <div>
           { msg.user }}: {{ msg.text }}
         </div>
     }

    <button (click)="loadOlder()">Load older</button>
  `,
})
export class ChatComponent {
  readonly messages = signal([
    { id: 1, user: 'Alice', text: 'Hello' },
    { id: 2, user: 'Bob', text: 'Hi' },
  ]);

  loadOlder() {
    const older = [
      { id: 0, user: 'System', text: `Message: ${this.messages().length}` },
    ];
    this.messages.set([...older, ...this.messages()]);
  }
}
Enter fullscreen mode Exit fullscreen mode

NgFor Using Index With New Data

Hope you liked the article, feel free to share your thoughts, catch more of my articles on So fail. Then fail better. And maybe one day, you'll be writing blog posts like this, after you've finished crying over your failed side project. Hope you liked these short stories. Feel free to share your thoughts, catch more of my articles on dev.to, connect with me on LinkedIn or check my Personal Website.

Top comments (0)