Como melhorar o desempenho angular – *ngFor trackBy

Está gostando? Compartilhe

A funcionalidade trackBy é usada no Angular para melhorar o desempenho das operações de manipulação de lista no Angular, como o ngFor.

O ngFor cria um loop através de uma lista e renderiza um elemento para cada item na lista. Ao adicionar ou remover itens da lista, o Angular deve atualizar a exibição para refletir essas alterações. No entanto, se o Angular não puder identificar rapidamente quais itens foram adicionados ou removidos, ele precisará atualizar toda a lista, o que pode ser lento em listas grandes.

É aqui que o trackBy entra em jogo. Ele fornece uma maneira de identificar rapidamente quais itens foram adicionados ou removidos, mesmo em listas grandes. O trackBy deve ser uma função que recebe o índice e item atual da lista, e deve retornar um valor único que identifica de forma exclusiva esse item na lista.

Por exemplo, se você estiver trabalhando com uma lista de objetos com uma propriedade id, você poderia usar o trackBy para retornar o valor da propriedade id como o identificador exclusivo do item na lista:

				
					<ul>
  <li *ngFor="let item of items; trackBy: trackById">{{item.name}}</li>
</ul>
				
			
				
					trackById(index: number, item: any) {
  return item.id;
}
				
			

Isso permitiria que o Angular identificasse rapidamente quais itens foram adicionados ou removidos com base em seus valores de id, em vez de ter que percorrer toda a lista para encontrar as diferenças.

Caso de uso ainda é um aplicativo da Web simples

O caso de uso ainda é um aplicativo da Web simples que exibe uma lista de livros. Agora, um formulário permitirá que o usuário insira um livro na lista, e os botões Excluir permitirão que o usuário remova itens da lista:

Nas postagens anteriores do blog dedicadas ao mesmo caso de uso – já mostramos os seguintes itens:

Para manter nosso aplicativo de exemplo relativamente simples, manteremos apenas a opção ChangeDetectionStrategy.OnPush, mas descartaremos a rolagem virtual. A diretiva  *cdkVirtualFor suporta a mesma função trackBy que o *ngFor trackBy, portanto, tudo o que veremos aqui ainda pode ser usado em conjunto com a rolagem virtual.

Vamos ver rapidamente do que se trata esta função trackBy.

Angular *ngFor trackBy

A diretiva estrutural *ngFor renderiza um modelo para cada item em uma coleção: 

<li *ngFor="let book of books">...</li>.

 

Alterar propagação

A diretiva NgForOf reage às alterações feitas no books iterador:

  • Adiciona um novo modelo ao DOM quando um livro é adicionado,
  • Remove um modelo do DOM quando um livro é excluído,
  • Reordena o DOM quando os livros são reordenados (caso não estudado nesta postagem do blog).

Por padrão, o Angular usa referências de objeto para rastrear itens na lista .

Agora imagine que nossa lista de livros é recuperada de um servidor distante usando HttpClient , e cada operação (criar ou excluir) atualiza toda a lista de livros. Todas as referências de objetos são perdidas sempre que uma ação é executada na lista, e o Angular precisa atualizar todo o DOM, mesmo que a maioria dos dados seja a mesma.

Para evitar essa operação custosa, podemos definir a função trackBy da diretiva  *ngFor e personalizar o algoritmo de rastreamento padrão: por exemplo, os books podem ser rastreados por seu ID em vez de sua referência de objeto.

Aplicativo de lista de books

Resumindo, nosso aplicativo de caso de uso:

  • tem uma lista de books usando *ngFor

Comportamento ngFor padrão baseado em referências de objeto para ilustrar problemas de desempenho com grande quantidade de dados.

  • Tem que adicionar e remover botões de book
  • Logs recém-criados BookComponent: um novo componente é criado quando Angular cria o modelo para atualizar o DOM,
  • Registra as alterações feitas no comprimento da lista para ver como ela se correlaciona com as atualizações do DOM

Vá para esta seção para obter os resultados.

De volta ao nosso aplicativo de exemplo de lista de livros!

Como usaremos a otimização de detecção de alteração OnPush, é preferível usar objetos imutáveis ​​para evitar armadilhas . Consulte esta postagem de blog dedicada à estratégia de detecção de alterações para saber por que a imutabilidade é obrigatória nesse caso.

Objeto de book e construtor

Então, vamos criar um bean imutável Book simples no arquivo book.ts:

				
					export class Book {
  constructor(public readonly id: string,
              public readonly title: string,
              public readonly author: string) {
  }
}
				
			
  1. E o construtor associado book-builder.ts:
				
					import {v4 as uuid} from 'uuid';
import {Book} from "./book";

export class BookBuilder {
  private idValue = uuid();
  private titleValue = 'title';
  private authorValue = 'author';

  public id(value: string): BookBuilder {
    this.idValue = value;
    return this;
  }

  public title(value: string): BookBuilder {
    this.titleValue = value;
    return this;
  }

  public author(value: string): BookBuilder {
    this.authorValue = value;
    return this;
  }

  from(book: Book): BookBuilder {
    return new BookBuilder()
      .id(book.id)
      .title(book.title)
      .author(book.author);
  }

  build(): Book {
    return new Book(
      this.idValue,
      this.titleValue,
      this.authorValue
    );
  }
}
				
			
  1. Este construtor permite criar facilmente um book com um identificador gerado automaticamente e valores padrão para o título e autor . A biblioteca uuid que gera IDs únicos pode ser instalada com os comandos:
				
					ubuntu@hugo:~/angular-performance-trackby$ npm install uuid
ubuntu@hugo:~/angular-performance-trackby$ npm install @types/uuid --save-dev
ubuntu@hugo:~/angular-performance-trackby$ npm install
				
			

Ebooks fake CRUD

  1. Para simular um servidor de back-end que contém nossa lista de livros, criamos um BookCrudService no arquivo book-crud.service.ts:
				
					import {Injectable} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {Book} from "./book";
import {BookBuilder} from "./book-builder";

@Injectable({
    providedIn: 'root'
})
export class BookCrudService {

    public readonly books = new BehaviorSubject<Book[]>([]);

    public load(length: number): void {
        this.books.next(Array.from({length}).map((value, index) => new BookBuilder().title(`Title ${index}`).author(`Author ${index}`).build()));
    }

    public create(book: Book): void {
        const books = this.booksClone;
        books.unshift(book);
        this.books.next(books);
    }

    public delete(book: Book): void {
        const books = this.booksClone;
        this.books.next(books.filter(current => current.id !== book.id));
    }

    private get booksClone(): Book[] {
        return this.books.value.map(book => new BookBuilder().from(book).build());
    }
}
				
			
  1. load() método inicializa a lista com um determinado número de livros.

    As operações de criação e exclusão atualizam a lista de livros clonando todo o seu conteúdo. Isso simula um servidor remoto que retornaria a lista atualizada de livros para qualquer modificação. Tal comportamento nos permite ver o problema de desempenho causado pelo algoritmo de rastreamento padrão da diretiva *ngFor .

    OnPush altera componentes de detecção

    A seguinte versão do componente Books list não usa a função trackBy 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,
})
export class BooksListComponent implements OnInit, OnDestroy {

    private readonly destroyed = new Subject<boolean>();

    books: Book[] = [];

    constructor(private crud: BookCrudService,
                private changeDetectorRef: 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();
    }
}
				
			

Apenas as alterações feitas nos valores @Input são detectadas com o modo OnPush. Como usamos a opção changeDetection: ChangeDetectionStrategy.OnPush no @Component decorator, devemos chamar this.changeDetectorRef.detectChanges() para forçar uma atualização da visão quando forem feitas alterações na lista de ebooks (ngOnInit).

Além disso, as alterações feitas na lista de ebooks são rastreadas por uma mensagem no console do navegador com console.log('BooksListComponent list changed', books.length);.

O modelo HTML books-list.component.html é simples:

				
					<ul>
    <li *ngFor="let book of books">
        <app-book [book]="book"></app-book>
    </li>
</ul>
				
			
  • A  diretiva *ngFor itera sobre a matriz books  exibe um componente app-book.

    BookComponent exibe cada título de livro individual e autor. Aqui está o modelo HTML ( book.component.html):

				
					{{book.title}} by <em>{{book.author}}</em>
<button style="margin-left: 10px" (click)="crud.delete(book)">Del.</button>
				
			
  • Um botão permite ao usuário remover um ebook individual da lista:

  • A  classe BookComponent ( book.component.ts) não precisa especificar o ChangeDetectionStrategy.OnPushjá que o componente pai BooksListComponentjá o declara:

				
					import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
import {Book} from "../book";
import {BookCrudService} from "../book-crud.service";

@Component({
    selector: 'app-book',
    templateUrl: './book.component.html',
    styleUrls: ['./book.component.scss']
})
export class BookComponent implements OnInit {

    @Input() book!: Book;

    constructor(public crud: BookCrudService) {
    }

    ngOnInit(): void {
        console.log('BookComponent ngOnInit', this.book);
    }
}
				
			

ngOnInit() rastreia a criação do componente do ebook registrando uma mensagem no console do navegador da web: console.log('BookComponent ngOnInit', this.book);. Usaremos isso mais tarde para verificar se muitos BookComponent são criados quando adicionamos um item à lista de livros.

Componente do aplicativo

Por fim, vamos agrupar tudo no AppComponent ( app.component.ts arquivo) cujo construtor inicializa a lista de ebooks com apenas 10 itens:

				
					import {Component} from '@angular/core';
import {FormControl, FormGroup} from "@angular/forms";
import {BookCrudService} from "./book-crud.service";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  titleControl = new FormControl<string>('New title', {nonNullable: true});
  authorControl = new FormControl<string>('New author', {nonNullable: true});
  formGroup = new FormGroup({
    title: this.titleControl,
    author: this.authorControl,
  })


  constructor(private crud: BookCrudService) {
    crud.load(10);
  }

  createBook(): void {
    this.crud.create(this.titleControl.value, this.authorControl.value);
  }
}
				
			

Modelo HTML app.component.html:

				
					<form [formGroup]="formGroup">
  <input type="text" [formControl]="titleControl">
  <input type="text" [formControl]="authorControl">
  <button (click)="createBook()">Create</button>
</form>

<app-books-list></app-books-list>
				
			

formGroup permite a criação de livros chamando a createBook() função:

Logging criando componente BookComponent

Agora podemos verificar quantos componentes BookComponent são criados toda vez que interagimos com nosso aplicativo .

A princípio, vemos nos logs do console:

  • o número de livros na lista
  • um BookComponent componente criado para cada um deles
Registros do console de carregamento do aplicativo

 

Até agora isso parece bom.

Logs do console na criação

Vamos clicar no botão Criar e ver o que acontece:

Criar logs do console do ebook
 

A lista de livros agora contém 11 ebooks como esperado. Mas, em vez de simplesmente criar um novo BookComponent, são criados 11.

Logs do console ao excluir

Novamente, ao clicar em um botão Del.:

Excluir logs do console do ebook

 

A lista de livros agora contém 9 livros como esperado. Mas aqui novamente 9 componentes BookComponent são criados.

Usando *ngFor trackBy

O TrackByFunction

Vamos atualizar nosso BooksListComponent para usar o TrackByFunction e tentar otimizar a quantidade de BookComponent criada quando a lista for atualizada ( books-list.component.ts):

				
					import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, TrackByFunction} from '@angular/core';
import {Subject, takeUntil} from "rxjs";
import {Book} from "../book";
import {BookCrudService} from "../book-crud.service";

@Component({
  selector: 'app-books-list',
  templateUrl: './books-list.component.html',
  styleUrls: ['./books-list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BooksListComponent implements OnInit, OnDestroy {

  private readonly destroyed = new Subject<boolean>();

  books: Book[] = [];

  constructor(private crud: BookCrudService,
              private changeDetectorRef: ChangeDetectorRef) {
  }

  trackByBookId: TrackByFunction<Book> = (index, book) => book.id;

  [...]  
}
				
			

Uma nova função trackByBookId é declarada e usada no modelo HTML associado books-list-trackby.component.html:

				
					<ul>
    <li *ngFor="let book of books; trackBy: trackByBookId">
        <app-book [book]="book"></app-book>
    </li>
</ul>
				
			

Isso TrackByFunction simplesmente retorna o Book.id campo a ser usado como um identificador.

Efeito do trackBy nos componentes criados

O comportamento ainda é o mesmo quando o aplicativo é carregado. O número de ebooks são exibidos e os componentes BookComponent são criados:

Registros do console de carregamento do aplicativo

 

O console faz logon criado usando trackBy

A melhoria pode ser vista quando criamos um novo livro:

Crie logs de console de livro com trackBy

 

Apenas um novo BookComponent foi criado!

O console registra ao excluir usando trackBy

Excluir um ebook também tem o comportamento esperado. Nenhum novo BookComponent é criado e o número de livros é reduzido em 1:

Console da lista de livros excluir trackBy

Excluir logs do console do livro com trackBy

Isso parece bom em termos de quantidade de componentes criados. Mas com uma pequena lista de apenas 10 itens não podemos ver nenhuma diferença em termos de desempenho.

Verificação de desempenho com Angular Devtools

Para obter resultados significativos, aumentamos o número de livros exibidos para 10.000 ( app.component.ts):

				
					export class AppComponent {
  constructor(private crud: BookCrudService) {
    crud.load(10000);
  }
}
				
			

Você também deve remover as chamadas para console.logno BookComponent ou ele ficará lento e talvez congele seu navegador da Web com um número tão grande de livros registrados!

Usaremos o Angular DevTools para medir quanto tempo leva para o Angular manipular as operações Create e Delete com e sem a função trackBy.

Medidas de desempenho sem o trackBy

Sem a função trackBy, leva um pouco menos de 5 segundos para o Angular lidar com um clique no botão Criar :

Sem trilha para Criar

 

Mesmos resultados para Del. clique: um pouco menos de 5 segundos:

Sem trackBy  Excluir

Medidas de desempenho com trackBy

Com a função trackBy podemos aumentar drasticamente as performances do nosso aplicativo! Leva apenas 203 milissegundos para o Angular manipular um clique no botão Criar :

Com trackBy Create

E apenas 152 milissegundos para lidar com um clique no botão Del

Conclusão

Estudamos como a diretiva NgForOf reage ao seu parâmetro iterador para atualizar o DOM. Por padrão, os itens são rastreados por sua referência de objeto e isso pode causar problemas de desempenho para listas grandes e/ou componentes renderizados complexos, fazendo muitas alterações na árvore DOM.

Vimos que usar uma trackBy função personalizada para a diretiva *ngFor nos permite personalizar o algoritmo de rastreamento padrão e melhorar muito o desempenho de um aplicativo Angular .

Quer ficar atualizado sobre marketing ?

Assine a nossa newsletter.

Aproveite e Veja Também

DESCUBRA NESSE E-BOOK 5 ESTRATÉGIAS PARA VENDER MUITO NA SUA LOJA DROPSHIPPING.

Você Quer Impulsionar Seu Negócio?

Mande-nos uma mensagem que entramos em contato.

Desenvolvimento de Backend com Node.js, TypeScript, MongoDB e Docker: Práticas Avançadas com TDD, DDD, Clean Architecture e SOLID

DESCUBRA como desenvolver um Backend com Node.js, TypeScript, MongoDB e Docker utilizando práticas avançadas com TDD, DDD, Clean Architecture e SOLID.