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.
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:
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:
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:
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:
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:
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.
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:
The benefits are obvious:
trackBy
function won't be needed, because the $implicit
context is always updatedThe 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. 🎉
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.
Minko Gechev: Faster Angular Applications — Part 1
Minko Gechev: Faster Angular Applications — Part 2
George is a developer on the Kendo UI team. His passion is frontend development and in his spare time he loves photography.