Como melhorar o desempenho do seu aplicativo Angular

Publicados: 2020-04-10

Ao falar sobre os melhores frameworks de frontend, é impossível não mencionar o Angular. No entanto, requer muito esforço dos programadores para aprendê-lo e usá-lo com sabedoria. Infelizmente, existe o risco de desenvolvedores sem experiência em Angular usarem alguns de seus recursos de forma ineficiente.

Uma das muitas coisas que você sempre precisa trabalhar como desenvolvedor front-end é o desempenho do aplicativo. Uma grande parte dos meus projetos anteriores se concentrou em grandes aplicativos corporativos que continuam a ser expandidos e desenvolvidos. Os frameworks frontend seriam extremamente úteis aqui, mas é importante usá-los de forma correta e razoável.

Eu preparei uma lista rápida das estratégias e dicas de aumento de desempenho mais populares que podem ajudá-lo a aumentar instantaneamente o desempenho do seu aplicativo Angular . Lembre-se de que todas as dicas aqui se aplicam ao Angular na versão 8.

ChangeDetectionStrategy e ChangeDetectorRef

A detecção de alterações (CD) é o mecanismo do Angular para detectar alterações de dados e reagir automaticamente a elas. Podemos listar o tipo básico de mudanças de estado do aplicativo padrão:

  • Eventos
  • Solicitação HTTP
  • Temporizadores

Essas são interações assíncronas. A questão é: como o Angular saberia que algumas interações (como clique, intervalo, solicitação http) ocorreram e há necessidade de atualizar o estado do aplicativo?

A resposta é ngZone , que é basicamente um sistema complexo destinado a rastrear interações assíncronas. Se todas as operações forem registradas pelo ngZone, o Angular saberá quando reagir a algumas alterações. Mas ele não sabe exatamente o que mudou e inicia o mecanismo de detecção de alterações , que verifica todos os componentes em primeira ordem de profundidade.

Cada componente do aplicativo Angular tem seu próprio Change Detector, que define como esse componente deve agir quando o Change Detection for iniciado – por exemplo, se houver a necessidade de renderizar novamente o DOM de um componente (o que é uma operação bastante cara). Quando o Angular inicia o Change Detection, todos os componentes serão verificados e sua visualização (DOM) poderá ser renderizada novamente por padrão.

Podemos evitar isso usando ChangeDetectionStrategy.OnPush:

 @Componente({
  seletor: 'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

Como você pode ver no código de exemplo acima, temos que adicionar um parâmetro adicional ao decorador do componente. Mas como essa nova estratégia de detecção de mudanças realmente funciona?

A estratégia diz ao Angular que um componente específico depende apenas de seu @Inputs(). Além disso, todo componente @Inputs() irá atuar como um objeto imutável (por exemplo, quando alteramos apenas a propriedade no @Input() de um objeto, sem alterar a referência, este componente não será verificado). Isso significa que muitas verificações desnecessárias serão omitidas e isso deve aumentar o desempenho do nosso aplicativo.

Um componente com ChangeDetectionStrategy.OnPush será verificado apenas nos seguintes casos:

  • A referência @Input() mudará
  • Um evento será acionado no template do componente ou em um de seus filhos
  • Observável no componente acionará um evento
  • O CD será executado manualmente usando o serviço ChangeDetectorRef
  • o pipe assíncrono é usado na visualização (o pipe assíncrono marca o componente a ser verificado quanto a alterações - quando o fluxo de origem emitir um novo valor, este componente será verificado)

Se nenhuma das situações acima acontecer, usar ChangeDetectionStrategy.OnPush dentro de um componente específico fará com que o componente e todos os componentes aninhados não sejam verificados após o lançamento do CD.

Felizmente, ainda podemos ter controle total da reação às alterações de dados usando o serviço ChangeDetectorRef. Temos que lembrar que com ChangeDetectionStrategy.OnPush dentro de nossos tempos limite, solicitações, retornos de chamada de assinaturas, precisamos disparar o CD manualmente se realmente precisarmos disso:

 contador = 0;

constructor(private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

Como podemos ver acima, chamando this.changeDetectorRef.detectChanges() dentro de nossa função timeout , podemos forçar o CD manualmente. Se o contador for usado dentro do modelo de alguma forma, seu valor será atualizado.

A última dica desta seção é sobre desabilitar permanentemente o CD para componentes específicos. Se tivermos um componente estático e tivermos certeza de que seu estado não deve ser alterado, podemos desabilitar o CD permanentemente:

 this.changeDetectorRef.detach()

Este código deve ser executado dentro do método de ciclo de vida ngAfterViewInit() ou ngAfterViewChecked(), para ter certeza de que nossa visualização foi renderizada corretamente antes de desabilitarmos a atualização de dados. Este componente não será mais verificado durante o CD , a menos que acionemos detectChanges() manualmente.

Chamadas de função e getters no modelo

O uso de chamadas de função dentro de modelos executa essa função toda vez que o Change Detector está em execução. A mesma situação acontece com getters . Se possível, devemos tentar evitar isso. Na maioria dos casos, não precisamos executar nenhuma função dentro do template do componente durante cada execução do CD . Em vez disso, podemos usar tubos puros.

Tubos puros

Os tubos puros são um tipo de tubos com uma saída que depende apenas de sua entrada, sem efeitos colaterais. Felizmente, todos os pipes em Angular são puros por padrão.

 @Cano({
    nome: 'maiúsculas',
    puro: verdadeiro
})

Mas por que devemos evitar usar pipes com pure: false? A resposta é Change Detection novamente. Pipes que não são puros são executados em cada execução de CD, o que não é necessário na maioria dos casos e diminui o desempenho do nosso app. Aqui está o exemplo da função que podemos alterar para pipe puro:

 transform(valor: string, limite = 60, reticências = '...') {
  if (!valor || valor.comprimento <= limite) {
    valor de retorno;
  }
  const numberOfVisibleCharacters = value.substr(0, limite).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

E vamos ver a vista:

 <p class="description">truncate(texto, 30)</p>

O código acima representa a função pura – sem efeitos colaterais, saída apenas dependente de entradas. Neste caso, podemos simplesmente substituir esta função por pipe puro :

 @Cano({
  nome: 'truncar',
  puro: verdadeiro
})
classe de exportação TruncatePipe implementa PipeTransform {
  transform(valor: string, limite = 60, reticências = '...') {
    ...
  }
}

E por fim, nesta view, obtemos o código, que será executado somente quando o texto for alterado, independentemente de Change Detection .

 <p class="description">{{ texto | truncar: 30 }}</p>

Módulos de carregamento lento e pré-carregamento

Quando seu aplicativo tem mais de uma página, você definitivamente deve considerar a criação de módulos para cada parte lógica do seu projeto, especialmente módulos de carregamento lento . Vamos considerar o código simples do roteador Angular:

 const rotas: Rotas = [
  {
    caminho: '',
    componente: HomeComponent
  },
  {
    caminho: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    caminho: 'barra',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  exportações: [RouterModule],
  importações: [RouterModule.forRoot(routes)]
})
classe AppRoutingModule {}

No exemplo acima podemos ver que o fooModule com todos os seus assets será carregado somente quando o usuário tentar entrar em uma rota específica (foo ou bar). Angular também irá gerar um pedaço separado para este módulo . O carregamento lento reduzirá a carga inicial .

Podemos fazer algumas otimizações adicionais. Vamos supor que queremos fazer nossos módulos de carregamento de aplicativos em segundo plano. Para este caso, podemos usar o preloadingStrategy. Angular por padrão tem dois tipos de preloadingStrategy:

  • Sem pré-carregamento
  • Pré-carregar todos os módulos

No código acima, a estratégia NoPreloading é usada por padrão. O aplicativo começa a carregar um módulo específico por solicitação do usuário (quando o usuário deseja ver uma rota específica). Podemos mudar isso adicionando alguma configuração extra ao roteador.

 @NgModule({
  exportações: [RouterModule],
  importações: [RouterModule.forRoot(routes, {
       preloadingStrategy: PreloadAllModules
  }]
})
classe AppRoutingModule {}

Essa configuração faz com que a rota atual seja mostrada o mais rápido possível e depois disso a aplicação tentará carregar os outros módulos em segundo plano. Inteligente, não é? Mas isso não é tudo. Se esta solução não atender às nossas necessidades, podemos simplesmente escrever nossa própria estratégia personalizada .

Vamos supor que queremos pré-carregar apenas módulos selecionados, por exemplo, BarModule. Indicamos isso adicionando um campo extra para o campo de dados.

 const rotas: Rotas = [
  {
    caminho: '',
    componente: HomeComponent
    dados: { pré-carregamento: falso }
  },
  {
    caminho: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    dados: { pré-carregamento: falso }
  },
  {
    caminho: 'barra',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    dados: { pré-carregamento: verdadeiro }
  }
]

Então temos que escrever nossa função de pré-carregamento personalizada:

 @Injetável()
export class CustomPreloadingStrategy implementa PreloadingStrategy {
  preload(route: Route, load: () => Observável<qualquer>): Observável<qualquer> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

E defina-o como um preloadingStrategy:

 @NgModule({
  exportações: [RouterModule],
  importações: [RouterModule.forRoot(routes, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
classe AppRoutingModule {}

Por enquanto apenas rotas com param { data: { preload: true } } serão pré-carregadas. O resto das rotas agirá como NoPreloading está definido.

Custom preloadingStrategy é @Injectable(), então significa que podemos injetar alguns serviços dentro se precisarmos e customizar nosso preloadingStrategy de qualquer outra forma.
Com as ferramentas de desenvolvedor de um navegador, podemos investigar o aumento de desempenho pelo tempo de carregamento inicial igual com e sem uma estratégia de pré-carregamento. Também podemos olhar para a guia de rede para ver se os pedaços de outras rotas estão sendo carregados em segundo plano, enquanto o usuário pode ver a página atual sem atrasos.

função trackBy

Podemos supor que a maioria dos aplicativos Angular usando *ngFor para iterar sobre os itens listados dentro do modelo. Se a lista iterada também for editável, trackBy é absolutamente obrigatório.

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(índice: número, produto: Produto) {
  devolver produto.id;
}

Ao usar a função trackBy, o Angular é capaz de rastrear quais elementos de coleções foram alterados (por determinado identificador) e renderizar novamente apenas esses elementos específicos. Quando omitimos trackBy, toda a lista será recarregada, o que pode ser uma operação que consome muitos recursos no DOM.

Compilação antecipada (AOT)

Sobre a documentação do Angular:

(…) componentes e modelos fornecidos pelo Angular não podem ser entendidos diretamente pelo navegador, os aplicativos Angular requerem um processo de compilação antes de serem executados em um navegador

Angular fornece os dois tipos de compilação:

  • Just-in-Time (JIT) – compila um aplicativo no navegador em tempo de execução
  • Ahead-of-Time (AOT) – compila um aplicativo em tempo de compilação

Para uso em desenvolvimento, a compilação JIT deve cobrir as necessidades do desenvolvedor. No entanto, para compilação de produção, definitivamente devemos usar o AOT . Precisamos ter certeza de que o sinalizador aot dentro do arquivo angular.json está definido como true. Os benefícios mais importantes de tal solução incluem renderização mais rápida, menos solicitações assíncronas, menor tamanho de download de estrutura e maior segurança.

Resumo

O desempenho do aplicativo é algo que você precisa ter em mente tanto durante o desenvolvimento quanto na parte de manutenção do seu projeto. No entanto, procurar soluções possíveis por conta própria pode consumir tempo e esforço. Verificar esses erros comumente cometidos e mantê-los em mente durante o processo de desenvolvimento não apenas ajudará você a melhorar o desempenho do seu aplicativo Angular rapidamente, mas também ajudará a evitar lapsos futuros.

Liberando o ícone do produto

Confie na Miquido com seu projeto Angular

Contate-Nos

Quer desenvolver um aplicativo com o Miquido?

Pensando em dar um up no seu negócio com um aplicativo Angular? Entre em contato conosco e escolha nossos serviços de desenvolvimento de aplicativos Angular.