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

Implemente uma 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 alterações 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 muitos cálculos 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 ignorando 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 analisar um app de exemplo.

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

O app lista 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 ver 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);
  }
}

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

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 em todos os funcionários da lista e, para cada um, renderiza um item de 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 individuais do departamento.

Quando o usuário começa a inserir o nome de um novo funcionário na caixa de entrada em um EmployeeListComponent, o Angular aciona a detecção de mudanças para toda a árvore de componentes, começando em AppComponent. Isso significa que, enquanto o usuário digita na 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 o DevTools.
  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 criação de perfil coletados, você verá algo assim:

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 para o usuário.

Ignorar subárvores de componentes

Quando o usuário está digitando na entrada de texto para vendas EmployeeListComponent, você sabe que os dados do 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 em uma entrada de texto, a detecção de mudanças só é acionada para o departamento correspondente:

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

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

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

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

Como usar pipes puros

Mesmo que a estratégia de detecção de mudanças para o EmployeeListComponent agora 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 na entrada de texto correspondente.

Para melhorar esse comportamento, use 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 só vai recalcular o resultado 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 realizando uma verificação de referência. Isso significa que o Angular não vai fazer nenhum recálculo, 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 encanamento é puro. O Angular vai considerar todos os pipes como puros, a menos que você especifique o contrário.

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

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

Pronto! Agora, quando o usuário digitar na 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 ver como a digitação ficou mais fluida.

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

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

Conclusão

Ao enfrentar lentidões de 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ões.
  2. Introduzir a estratégia de detecção de mudanças OnPush para remover as subárvores de um componente.
  3. Mova cálculos pesados para pipes puros para permitir que a estrutura execute o armazenamento em cache dos valores calculados.