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

Implemente uma detecção de alterações 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 alterações pode ser acionada manualmente ou por meio de um evento assíncrono (por exemplo, uma interação do usuário ou uma conclusão XHR).

A detecção de alterações é uma ferramenta poderosa, mas, se executada com frequência, pode acionar diversos cálculos e bloquear a linha de execução principal do navegador.

Nesta postagem, você aprenderá a controlar e otimizar o mecanismo de detecção de alterações ignorando partes do seu aplicativo e executando a detecção de alterações somente quando necessário.

Por dentro da detecção de mudanças do Angular

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

O código do app está disponível neste repositório do GitHub.

O aplicativo 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

É possível ver as duas instâncias de EmployeeListComponent no modelo para 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 visualiza o resultado na tela.

Agora dê uma olhada em 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 da empresa.

Este é 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 por todos os funcionários na lista e, para cada um deles, renderiza um item da lista. Ela também inclui uma diretiva ngModel para 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 do departamento.

Quando o usuário começa a digitar o nome de um novo funcionário na caixa de entrada em uma EmployeeListComponent, o Angular aciona a detecção de mudanças em toda a árvore de componentes começando em 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.

Verifique se a lentidão vem da função fibonacci configurando o projeto de exemplo e abrindo a guia Desempenho do Chrome DevTools.

  1. Pressione "Control+Shift+J" (ou "Command+Option+J" no Mac) para abrir o DevTools.
  2. Clique na guia Desempenho.

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

Criação de perfis de desempenho

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

Como pular 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 alterações, defina 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 digitar uma entrada de texto, a detecção de mudanças só vai ser acionada para o departamento correspondente:

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

Encontre essa otimização aplicada ao aplicativo original aqui.

Saiba mais sobre a estratégia de detecção de mudanças do OnPush na documentação oficial do Angular (em inglês).

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

O uso de pipes puros

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

Para melhorar esse comportamento, você pode usar 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 recalculará seu resultado somente se receber uma entrada diferente da invocação anterior.

Lembre-se de que o app calcula um valor a ser exibido 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 recalculará a expressão de barra vertical somente quando os argumentos mudarem. Para determinar se os argumentos do pipe foram alterados, o framework faz uma verificação de referência. Isso significa que o Angular não vai realizar recálculos, a menos que o valor numérico de um funcionário seja atualizado.

Veja como mover o cálculo empresarial 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 tubo está limpo. O Angular vai considerar todos os pipelines como puros, a menos que você especifique o contrário.

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

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

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

No app abaixo, você pode ver como a digitação é mais suave.

Para ver o efeito da última otimização, teste este exemplo no StackBlitz.

O código com a otimização pura de pipeline do aplicativo original está disponível aqui.

Conclusão

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

  1. Criar perfil do aplicativo com o Chrome DevTools para ver a origem da lentidão.
  2. Introduzir a estratégia de detecção de mudanças OnPush para podar as subárvores de um componente.
  3. Mover cálculos pesados para pipes puros a fim de permitir que o framework execute o armazenamento em cache dos valores calculados.