Telerik blogs
Blazing_870x220

Understand how NgFor works in Angular, and how it can be further customized to squeeze out an additional 30-50% improvement to your rendering performance.

Angular has a clever way to control the template structure of a component; through structure directives like NgIf, NgForOf, and NgSwitch. In this post, we will concentrate on NgForOf, because - despite some of its disadvantages - it is the mainstream way of rendering an iterable.

How Does NgForOf Work?

The documentation states:

The NgForOf directive instantiates a template once per item from an iterable. The context for each instantiated template inherits from the outer context with the given loop variable set to the current item from the iterable.

Each template instance will be created with an implicit context bound to each data item. This is done in the applyChanges method. The most interesting part is the result of the IterableDiffer instance, which determines whether a new template instance should be created, removed or moved. NgForOf will remove, create or move the template instances based on the reported changes. These are the specific code snippets that handle the specific updates.

If an item is added, a new template instance will be created:

if (item.previousIndex == null) {
  const view = this._viewContainer.createEmbeddedView(
    this._template,
    new NgForOfContext<t>(null !, this.ngForOf, -1, -1),
    currentIndex);
  const tuple = new RecordViewTuple<t>(item, view);
  insertTuples.push(tuple);
}

If an item is removed, the corresponding template will be removed:

} else if (currentIndex == null) {
  this._viewContainer.remove(adjustedPreviousIndex);
} else {

If an item is moved, the template instance will be moved:

} else {
  const view = this._viewContainer.get(adjustedPreviousIndex) !;
  this._viewContainer.move(view, currentIndex);
  const tuple = new RecordViewTuple(item,
    <embeddedviewref<ngforofcontext<t>>>view);
  insertTuples.push(tuple);
}

As we can see, NgForOf will update the DOM on every change. By default, it will compare list items by reference. This isn't terribly efficient, even if immutable data is used. Basically, a change will be detected whenever the item reference updates. This includes item structures or values that remain unchanged.

Let's assume the following example built in Angular:

Here's what this example looks like in Chrome DevTools:

NgForOf in Chrome Tools

Common Approaches to Optimize NgForOf

The aforementioned case can be easily handled by a custom trackBy function, which defines the diffing mechanism. Instead of comparing references, we can check for the relevant property values:

<ul>
  <li *ngFor="let item of data; trackBy: trackData">
    <span data-id="{{ item.value }}">{{ item.value }}</span>
  </li>
</ul>

public trackData(_: number, item: any): any {
  return item.value;
}

Let's assume the another example built in Angular:

Here's what this example looks like in Chrome DevTools:

NgForOf in Chrome Tools

Everything looks OK, but we have a problem. The trackBy function won't help when the data has actually changed and we use custom components. In this case, NgForOf will destroy the old component and create a new one for every change.

Let's assume a third example:

Here's what this example looks like in Chrome DevTools:

NgForOf in Chrome Tools

Notice how that the whole <li> is recreated on change. Basically, the directive will remove the old DOM element and will add new one even though only the dataItem.value has changed.

As you can see, we don't do anything fancy here. We simply wish to do the following:

  • use the same template instance
  • update only the template internals
  • reduce unnecessary DOM updates

The Naïve Solution

The first thing that we came up with was to "unfold" the loop and use N-times NgIf directives. This requires copying the template n-times and passing every data item by index. If you can't imagine it, I don't blame you, it is not the brightest idea.

template: `
  <div>
    <button (click)="data = next()">Move to next page
    <h3>Data list</h3>
    <item *ngif="data[0]" [instance]="data[0].instance">
    <item *ngif="data[1]" [instance]="data[1].instance">
    <item *ngif="data[2]" [instance]="data[2].instance">
    <item *ngif="data[3]" [instance]="data[3].instance">
    <item *ngif="data[4]" [instance]="data[4].instance">
    <item *ngif="data[5]" [instance]="data[5].instance">
    <item *ngif="data[6]" [instance]="data[6].instance">
    <item *ngif="data[7]" [instance]="data[7].instance">
    <item *ngif="data[8]" [instance]="data[8].instance">
    <item *ngif="data[9]" [instance]="data[9].instance">
  </div>
`,

Let's assume a fourth example:

Here's what this example looks like in Chrome DevTools:

NgForOf in Chrome Tools

Surprisingly, this works because DOM nodes are not removed; only the corresponding bindings are updated. If the displayed data has a fixed maximum length (i.e. 30 items) then the duplicated templates with NgIf could be a suitable solution.

The main concern here is the size of the template. This will slow down the compilation (a real nightmare for your CI) and will produce a larger runtime footprint.

The Real Solution

A smarter way to resolve the issue will be to combine the benefits of both NgForOf and NgIf directives and remove their disadvantages. Basically, we just need to build a custom NgForOf directive. It will still use the default IteratableDiffer, but the DOM updates will be handled differently. The directive won't remove the template instance if there is a data item for it. It will add new templates when data exceeds current structure and will remove template instances when there are no items for them. Here is the _applyChanges method, which implements the desired behavior:

private _applyChanges(changes: IterableChanges<T>): void {
  const viewContainerLength = this._viewContainer.length;
  const dataLength = (<any>this.myForOf).length;
  const tuples: any = {};

  // gather all new data items
  changes.forEachOperation(
    (record: IterableChangeRecord<any>, _: number, currentIndex: number) => {
      if (currentIndex !== null) {
        tuples[currentIndex] = record.item;
      }
    }
  );

  // create template instances
  for (let i = viewContainerLength; i < dataLength; i++) {
    this._viewContainer.createEmbeddedView(this._template,
      new MyForOfContext<T>(null !, this.myForOf, -1, -1),
      i);
  }

  // remove template instances
  for (let i = this._viewContainer.length; i > dataLength; i--) {
    this._viewContainer.remove(i);
  }

  // update templates context
  for (let i = 0; i < this._viewContainer.length; i++) {
    const view = <EmbeddedViewRef<MyForOfContext<T>>>this._viewContainer.get(i) !;
    view.context.index = i;
    view.context.count = length;
    view.context.$implicit = tuples[i] || null;
  }
}

Let's assume a fifth example:

Here's what this example looks like in Chrome DevTools:

NgForOf in Chrome Tools

The benefits are obvious:

  • the template is kept small
  • it works with arbitrary data length
  • custom trackBy function won't be needed, because the $implicit context is always updated
  • the content will be rendered faster, because it doesn't recreate template instances

The drawback is that the item change cannot be animated using an enter or leave animation.

To see the directive in action, check the Calendar component in Kendo UI for Angular. It uses UI virtualization to display months and rendering performance is crual for the smooth scrolling. Our measurements showed that we gained 30-50% rendering improvement, which basically made the component usable in Internet Explorer 11. 🎉

Conclusion

We found out that Angular could be tweaked to render even faster. Even though that the proposed custom implementation has its limitations, it gives a 30-50% improvement to your rendering time. I will skip animations all day long if the component renders faster.

Further Reading

Minko Gechev: Faster Angular Applications — Part 1

Minko Gechev: Faster Angular Applications — Part 2


Georgi Krustev
About the Author

Georgi Krustev

George is a developer on the Kendo UI team. His passion is frontend development and in his spare time he loves photography.

Comments

Comments are disabled in preview mode.