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:
- {{item.name}}
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:
- como usar a
OnPush
estratégia de detecção de mudanças para melhorar desempenhos , - como usar a rolagem virtual para reduzir o número efetivo de itens renderizados .
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) {
}
}
- 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
);
}
}
- 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
- Para simular um servidor de back-end que contém nossa lista de livros, criamos um
BookCrudService
no arquivobook-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([]);
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());
}
}
-
O
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çãoA 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();
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:
-
-
A diretiva
*ngFor
itera sobre a matrizbooks
exibe um componenteapp-book
.BookComponent
exibe cada título de livro individual e autor. Aqui está o modelo HTML (book.component.html
):
{{book.title}} by {{book.author}}
-
Um botão permite ao usuário remover um ebook individual da lista:
-
A classe
BookComponent
(book.component.ts
) não precisa especificar oChangeDetectionStrategy.OnPush
já que o componente paiBooksListComponent
já 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);
}
}
O 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('New title', {nonNullable: true});
authorControl = new FormControl('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
:
O 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
Até agora isso parece bom.
Logs do console na criação
Vamos clicar no botão Criar e ver o que acontece:
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.:
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();
books: Book[] = [];
constructor(private crud: BookCrudService,
private changeDetectorRef: ChangeDetectorRef) {
}
trackByBookId: TrackByFunction = (index, book) => book.id;
[...]
}
Uma nova função trackByBookId
é declarada e usada no modelo HTML associado books-list-trackby.component.html
:
-
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:
O console faz logon criado usando trackBy
A melhoria pode ser vista quando criamos um novo livro:
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:
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.log
no 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 :
Mesmos resultados para Del. clique: um pouco menos de 5 segundos:
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 :
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 .