Otimizar a detecção de alterações do Angular's

Implemente a detecção de mudanças mais rápida para melhorar a experiência do usuário.

O Angular executa o mecanismo de detecção de mudanças periodicamente para que as mudanças no modelo de dados sejam refletidas na visualização de um app. A detecção de mudanças pode ser acionada manualmente ou por um evento assíncrono (por exemplo, uma interação do usuário ou uma conclusão de XHR).

A detecção de mudanças é uma ferramenta poderosa, mas, se for executada com muita frequência, pode acionar muitas computações e bloquear a linha de execução principal do navegador.

Nesta postagem, você vai aprender a controlar e otimizar o mecanismo de detecção de mudanças pulando partes do aplicativo e executando a detecção de mudanças apenas quando necessário.

Dentro da detecção de mudanças do Angular

Para entender como a detecção de mudanças do Angular funciona, vamos conferir um app de exemplo.

Encontre o código do app neste repositório do GitHub.

O app lista os funcionários de dois departamentos de uma empresa, vendas e P&D, e tem dois componentes:

  • AppComponent, que é o componente raiz do app, e
  • Duas instâncias de EmployeeListComponent, uma para vendas e outra para P&D.

Exemplo de aplicativo

Você pode conferir as duas instâncias de EmployeeListComponent no modelo de AppComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Para cada funcionário, há um nome e um valor numérico. O app transmite o valor numérico do funcionário para um cálculo comercial e mostra o resultado na tela.

Agora confira EmployeeListComponent:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent aceita uma lista de funcionários e um nome de departamento como entradas. Quando o usuário tenta remover ou adicionar um funcionário, o componente aciona uma saída correspondente. O componente também define o método calculate, que implementa o cálculo de negócios.

Confira o modelo para EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Esse código itera em todos os funcionários na lista e, para cada um, renderiza um item de lista. Ele também inclui uma diretiva ngModel para a vinculação de dados bidirecional entre a entrada e a propriedade label declarada em EmployeeListComponent.

Com as duas instâncias de EmployeeListComponent, o app forma a seguinte árvore de componentes:

Árvore de componentes

AppComponent é o componente raiz do aplicativo. Os componentes filhos são as duas instâncias de EmployeeListComponent. Cada instância tem uma lista de itens (E1, E2 etc.) que representam os funcionários individuais do departamento.

Quando o usuário começa a digitar o nome de um novo funcionário na caixa de entrada em umEmployeeListComponent, o Angular aciona a detecção de mudanças para toda a árvore de componentes, começando por AppComponent. Isso significa que, enquanto o usuário digita a entrada de texto, o Angular recalcula repetidamente os valores numéricos associados a cada funcionário para verificar se eles não mudaram desde a última verificação.

Para ver como isso pode ser lento, abra a versão não otimizada do projeto no StackBlitz e tente inserir o nome de um funcionário.

Para verificar se a lentidão vem da função fibonacci, configure o projeto de exemplo e abra a guia Performance do Chrome DevTools.

  1. Pressione "Control+Shift+J" (ou "Command+Option+J" no Mac) para abrir as Ferramentas do desenvolvedor.
  2. Clique na guia Performance.

Agora clique em Gravar (no canto superior esquerdo do painel Performance) e comece a digitar em uma das caixas de texto do app. Em alguns segundos, clique em Gravar novamente para interromper a gravação. Depois que o Chrome DevTools processar todos os dados de perfil coletados, você verá algo como isto:

Criação de perfis de desempenho

Se houver muitos funcionários na lista, esse processo poderá bloquear a linha de execução da interface do navegador e causar quedas de frames, o que leva a uma experiência ruim do usuário.

Como ignorar subárvores de componentes

Quando o usuário está digitando a entrada de texto para o EmployeeListComponent de vendas, você sabe que os dados no departamento de P&D não estão mudando. Portanto, não há motivo para executar a detecção de mudanças no componente. Para garantir que a instância de P&D não acione a detecção de mudanças, defina o changeDetectionStrategy de EmployeeListComponent como OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Agora, quando o usuário digita uma entrada de texto, a detecção de mudanças é acionada apenas para o departamento correspondente:

Detecção de mudanças em um subárvore de componentes

Confira esta otimização aplicada ao app original neste link.

Leia mais sobre a estratégia de detecção de mudanças do OnPush na documentação oficial do Angular.

Para conferir o efeito dessa otimização, insira um novo funcionário no aplicativo StackBlitz.

Como usar pipes puros

Mesmo que a estratégia de detecção de mudanças para o EmployeeListComponent esteja definida como OnPush, o Angular ainda recalcula o valor numérico para todos os funcionários de um departamento quando o usuário digita a entrada de texto correspondente.

Para melhorar esse comportamento, aproveite os pipes puros. Os pipes puros e impuros aceitam entradas e retornam resultados que podem ser usados em um modelo. A diferença entre os dois é que um pipe puro vai recalcular o resultado somente se receber uma entrada diferente da invocação anterior.

O app calcula um valor a ser mostrado com base no valor numérico do funcionário, invocando o método calculate definido em EmployeeListComponent. Se você mover o cálculo para um pipe puro, o Angular vai recalcular a expressão do pipe somente quando os argumentos mudarem. O framework vai determinar se os argumentos do pipe mudaram ao realizar uma verificação de referência. Isso significa que o Angular não vai realizar novos cálculos, a menos que o valor numérico de um funcionário seja atualizado.

Veja como mover o cálculo de negócios para um pipe chamado CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

O método transform do pipe invoca a função fibonacci. Observe que o pipe é puro. O Angular considera todos os pipes puros, a menos que você especifique o contrário.

Por fim, atualize a expressão no modelo para EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Pronto! Agora, quando o usuário digitar a entrada de texto associada a qualquer departamento, o app não vai recalcular o valor numérico para funcionários individuais.

No app abaixo, você pode conferir como a digitação fica mais fácil.

Para conferir o efeito da última otimização, tente este exemplo no StackBlitz.

O código com a otimização de pipe puro do aplicativo original está aqui.

Conclusão

Ao enfrentar lentidão no tempo de execução em um app Angular:

  1. Crie um perfil do aplicativo com o Chrome DevTools para saber de onde vêm as lentidão.
  2. Apresente a estratégia de detecção de mudanças OnPush para podar os subárvores de um componente.
  3. Mova as computações pesadas para pipes puros para permitir que a estrutura armazene em cache os valores calculados.