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 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, ela 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 ver um app de exemplo.

O código do app está disponível 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

É 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 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.

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 é iterado 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 as Ferramentas do desenvolvedor.
  2. Clique na guia Desempenho.

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

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

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 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.

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 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 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 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 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. 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.