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 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 executada com muita frequência, pode acionar vários 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 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 (em inglês).

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

É possível 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 de negócios e exibe o resultado na tela.

Agora, observe 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 dos 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 itera em todos os funcionários na lista e, para cada um, 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 declaradas 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 uma EmployeeListComponent, o Angular aciona a detecção de mudanças em toda a árvore de componentes a partir de 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.

É possível verificar 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.

novamente para interromper a gravação. Assim 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 de IU do navegador e causar quedas de frames, resultando em uma experiência do usuário negativa.

Como pular subárvores de componentes

Quando o usuário digita a entrada de texto da EmployeeListComponent de vendas, você sabe que os dados do departamento de P&D não mudam, então não há motivo para executar a detecção de alterações 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 só é acionada para o departamento correspondente:

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

Você pode encontrar essa otimização aplicada à inscrição original aqui.

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

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

Como usar pipes puros

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

Para melhorar esse comportamento, use pipes. Tanto os pipelines puros quanto os 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 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 uma barra vertical pura, o Angular recalculará a expressão de barra vertical somente quando os argumentos dela mudarem. O framework vai determinar se os argumentos do pipe foram alterados por 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.

Confira como mover o cálculo de negócios para uma barra vertical chamada 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 considerará todos os pipes 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 a entrada de texto associada a qualquer departamento, o app não vai recalcular o valor numérico de cada funcionário.

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

Para ver o efeito da última otimização, veja 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ão no tempo de execução em um aplicativo do Angular:

  1. Crie um perfil do aplicativo com o Chrome DevTools para entender a causa da lentidão.
  2. Introdução da estratégia de detecção de mudanças OnPush para remover as subárvores de um componente.
  3. Mover cálculos pesados para pipes puros para permitir que o framework realize o armazenamento em cache dos valores computados.