Yet another blog post dedicated to Angular performance optimizations.
This time we will use the trackBy function of the *ngFor structural directive to optimize changes made to the DOM tree and thus optimize performances when updating a large list of items.
The use case is still a simple web application that displays a list of books.
Now, a form will let the user insert a book in the list, and Delete buttons will let the user remove items from the list:
To keep our example application relatively simple we will only keep the ChangeDetectionStrategy.OnPush option but drop the virtual-scrolling.
The *cdkVirtualFor directive supports the same trackBy function as the *ngFor trackBy, so all we will see here could still be used conjointly with virtual-scrolling.
Let's quickly see what this trackBy function is about!
Adds a new template to the DOM when a book is added,
Removes a template from the DOM when a book is deleted,
Reorders the DOM when books are reordered (case not studied in this blog post).
By default, Angular uses object references to track items in the list.
Now imagine our book list is retrieved from a distant server using HttpClient, and each operation (create or delete), refreshes the whole book list.
All object references are lost each time an action is done on the list, and Angular has to refresh the whole DOM even if most of the data is the same.
To avoid this expensive operation, we can set the trackBy function of the *ngFor directive and customize the default tracking algorithm:
for example books can be tracked by their ID instead of their object reference.
Since we will be using the OnPush change detection optimization, it's preferable to use immutable objects to avoid pitfalls.
Please refer to this blog post dedicated to change detection strategy to know why immutability is a must-have in such case.
This builder allows to easily create a book with an auto-generated identifier and default values for the title and author.
The uuid library that generates unique IDs can be installed with the commands:
This builder also allows us to update an existing book using the from() method.
Indeed, the Book being immutable, we must use the builder to create a new updated instance: new BookBuilder().from(book).title('new title').build().
The load() method initializes the list with a given number of books.
Create and delete operations both update the book list by cloning all of its content.
This simulates a remote server that would return the updated list of books for any modification.
Such behavior allows us to see the performance problem caused by the default tracking algorithm of the *ngFor directive.
The following version of the Books list component does not use the trackBy function (books-list.component.ts):
import{ChangeDetectionStrategy,ChangeDetectorRef,Component,OnDestroy,OnInit}from'@angular/core';import{BookCrudService}from"../book-crud.service";import{Book}from"../book";import{Subject,takeUntil}from"rxjs";@Component({selector:'app-books-list',templateUrl:'./books-list.component.html',styleUrls:['./books-list.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush,})exportclassBooksListComponentimplementsOnInit,OnDestroy{privatereadonlydestroyed=newSubject<boolean>();books:Book[]=[];constructor(privatecrud:BookCrudService,privatechangeDetectorRef:ChangeDetectorRef){}ngOnInit():void{this.crud.books.pipe(takeUntil(this.destroyed)).subscribe(books=>{console.log('BooksListComponent list changed',books.length);this.books=books;this.changeDetectorRef.detectChanges();});}ngOnDestroy():void{this.destroyed.next(true);this.destroyed.complete();}}
Only changes made to @Input values are detected with the OnPush mode.
As we use the changeDetection: ChangeDetectionStrategy.OnPush option in the @Component decorator,
we must call this.changeDetectorRef.detectChanges() to force a refresh of the view when changes are made to the books list (ngOnInit).
Also, changes made to the books list are tracked by a message in the browser console with console.log('BooksListComponent list changed', books.length);.
The HTML template books-list.component.html is simple:
<ul><li*ngFor="let book of books"><app-book[book]="book"></app-book></li></ul>
The *ngFor directive iterates over the books array and display a app-book component.
BookComponent displays each individual book title and author.
Here is the HTML template (book.component.html):
{{book.title}} by <em>{{book.author}}</em><buttonstyle="margin-left: 10px"(click)="crud.delete(book)">Del.</button>
A button allows the user to remove an individual book from the list:
The BookComponent class (book.component.ts) does not need to specify the ChangeDetectionStrategy.OnPush since the parent BooksListComponent component already declares it:
The ngOnInit() tracks the book component creation by logging a message in the web browser console: console.log('BookComponent ngOnInit', this.book);.
We will use this later on to check on many BookComponent are created when we add an item to the books list.
Let's update our BooksListComponent to use the TrackByFunction and try to optimize the amount of BookComponent created when the list is updated (books-list.component.ts):
Deleting a book also has the expected behavior. No new BookComponent is created and the number of books is reduced by 1:
That looks good in terms of quantity of created components.
But with a small list of only 10 items we cannot see any difference in terms of performance.
You should also remove the calls to console.log in the BookComponent or it will slow down and maybe freeze your web browser with such a large number of books logged!
We will be using Angular DevTools to measure how long it takes for Angular to handle Create and Delete operations with and without the trackBy function.
With the trackBy function we can drastically increase the performances of our application!
It takes only 203 milliseconds for Angular to handle a click on the Create button:
And only 152 milliseconds to handle a click on the Del. button:
We studied how the NgForOf directive reacts to its iterator parameter to update the DOM.
By default, items are tracked by their object reference, and that can cause performance issues for big lists and/or complex rendered components by making to many changes to the DOM tree.
We saw that using a custom trackBy function for the *ngFor directive allows us to customize the default tracking algorithm and greatly improve the performance of an Angular application.