Melhorando a performance em aplicações Angular

Melhorando a performance em aplicações Angular

Afinal, o que é performance? O que faz um usuário perceber que uma aplicação está com boa performance ou não? Diz a lenda que só colocaram um espelho dentro dos elevadores para ele parecer mais rápido, afinal, quem entra no elevador fica distraído com ele e o tempo parece passar mais rápido.

Isso se chama performance percebida. As vezes retirar um loading que bloqueia uma tela inteira e colocar o loading diretamente dentro de uma tabela carregando os dados faz parecer que tudo funcionou mais rápido, pois o usuário já podia mexer em outras funcionalidades, mas a verdade é que os dados carregaram no mesmo tempo.

Quando falamos de performance para aplicações web, estamos falando de um conceito muito amplo, então aqui vamos focar em como melhorar a performance da sua aplicação em três pontos principais:

- Carregar a aplicação.
- Execução.
- Memória.

Performance para carregar a aplicação

Carregar uma aplicação SPA como o Angular é sempre um desafio, são diversos arquivos Javascript e várias técnicas que podem ser aplicadas, o próprio framework do Angular já nos oferece alguns modos de melhorar o modo como carregamos a aplicação no navegador. O que vamos focar aqui então é o Time-to-Interact, que dita quanto tempo vai demorar desde que o usuário digitou a URL para entrar na aplicação até ele poder fazer a primeira interação.

Ivy compiler

A dica aqui praticamente é: tenha a versão do Angular atualizada.

Desde a versão 9 do Angular, o time vem mudando o compilador principal chamado View Engine para o novo Ivy, que é altamente recomendado não o desativar por mais que pode ser desativado manualmente. O novo compilador oferece um tree shaking muito mais otimizado para a sua aplicação e deve ser usado em junção ao AOT (aot=true no seu angular.json). Para obter ajuda como atualizar a sua aplicação Angular, siga a ferramenta oficial.

Brotli

O Brotli é um compressor de dados de código aberto do Google e já é aceito na maioria dos browsers:

O Brotli pode ser usado para comprimir respostas HTTPS enviadas ao browser no lugar do gzip ou deflate. Essa configuração deve ser feita no seu WebServer e alguns deles já vem com essa configuração habilitada.

Para saber se a sua aplicação já no servidor está com o Brotli ligado, abra a aplicação, entre no DevTools e busque pela chamada dos JavaScripts. O próprio site do caniuse.com não tem o Brotli ligado, apenas o gzip:

Já os JavaScripts da página de documentação do angular.io vai mostrar o br (Brotli):

Uma aplicação pode ter várias compactações configuradas enquanto o WebServer vai sempre entregar o menor e mais compactado arquivo.

GZIP

O Gzip é o compactador default. Todos os WebServers já possuem um modo simples de configurar e customizar para compactar apenas o que você precisa. Além dos arquivos Javascript da aplicação, o Gzip pode e deve ser usado para compactar todas as respostas HTTP, então o seu back-end deve ser configurado para entregar as respostas das API’s já com o Gzip ligado (fácil de ser configurado no SpringBoot ou .Net por exemplo).

A configuração recomendada para uma aplicação Angular é que os assets devam ser configurados pelos seguintes MIME types:

● application/json.
● text/html.
● text/css.

E as principais customizações importantes que podem ser feitas são:

● MIME Types para serem comprimidos (text/html, application/json etc.).
● Tamanho mínimo do arquivo que o WebServer vai considerar para comprimir ou não um determinado arquivo, geralmente 1024 bytes. Se o WebServer tentar comprimir um arquivo menor que isso, pode ser que fique maior do que não comprimido.
● GZIP estático: alguns WebServers possuem um modo de configurar para comprimir um arquivo apenas na primeira vez que for chamado e então armazenar no servidor para ser entregue nas próximas solicitações, economizando tempo sem precisar comprimir toda vez.


Lazy Load

Por padrão, os módulos Angular que são importados diretamente no bloco imports do AppModule do Angular serão incluídos no arquivo JavaScript principal (main.js), exemplo:

Porém, para grandes aplicações, pode ser que você não queira carregar todos os módulos e todas as funcionalidades ao iniciar a aplicação.

Para isso, você deve mudar o modo como esse módulo vai ser carregado para ser carregado apenas quando determinada rota for chamada, então remova dos imports e mova para o seu arquivo de rotas utilizando o método loadChildren para tal:

Desta forma, o módulo MyModule e todos os componentes declarados nele não ficarão dentro do main.js (será criado um js apenas para ele) e esse JavaScript só será chamado no navegador ao acessar a rota myroute.

OBS: Nunca configure Lazy Load para a rota principal.

Vamos supor que você tenha a seguinte configuração de rotas:

Ao acessar a sua aplicação na primeira rota (https://example.com/), a sua aplicação vai te redirecionar para a rota dashboard, fazendo com que o browser mesmo já tendo baixado o arquivo principal para o primeiro render (main.js) vai ter que esperar por mais um download (mymodule.js) apenas para mostrar o primeiro conteúdo.

Nunca coloque a primeira rota a ser chamada em Lazy Load, pois isso afetará diretamente o carregamento da sua aplicação.

Pré -carregue o Lazy Load

A última coisa que você pode fazer em relação ao Lazy Load é pré-carregar as rotas depois da primeira renderização. Isso fará que a experiência do usuário ao usar a aplicação seja mais rápida, visto que todos as outras rotas visíveis já estarão prontas para uso no browser do usuário (isso não impacta o primeiro carregamento, apenas melhorará a performance ao carregar os outros módulos).

Para fazer essa configuração, você pode usar a ferramenta ngx-quicklinks: https://github.com/mgechev/ngx-quicklink

Cache

Uma prática muito comum é o cache de arquivos estáticos. O próprio browser já vai por padrão colocar os arquivos da sua aplicação (js, css, html, fonts, imagens etc.) no cache local, porém existem diversas formas de controlar o cache dos arquivos. Aqui pretendo listar alguns dos mais importantes para você se aprofundar mais, sendo que só o cache dá um artigo inteiro.

Alguns modos que você pode se aprofundar para trabalhar melhor com cache:

● Service Workers: com eles você pode incluir no cache não apenas arquivos como também resultados de API
● Cache no nível do WebServer: Adicionando e trabalhando melhor com o header Cache-Control
● Cache no nível do browser: gerenciamento do que você quer ou não incluir no Cache Storage do browser, para isso veja a API aqui.

Performance da aplicação em execução


Certo, então a aplicação já carrega rápido, mas quando o usuário clica em um botão para fazer alguma interação ele ainda parece lento, como podemos melhorar?

enableProdMode()

Uma coisa que devemos verificar é se o método enableProdMode() está sendo chamado no arquivo main.ts, sem esse método o Angular vai executar vários Change Detections para te auxiliar no desenvolvimento, chamar esse método em Dev só vai atrapalhar o seu desenvolvimento, então verifique que no seu arquivo o método só é chamado para produção:

Web Workers

Um problema comum em aplicações SPA é o fato de todo o JavaScript estar sendo executado na única thread do browser. Em aplicações complexas e com uma grande árvore de componentes aonde a detecção de mudanças precisa executar milhares de verificações a cada segundo, facilmente a performance começará a cair.

Os Web Workers funcionam como uma novo thread no browser, você pode demandar pra ele processos lentos e que podem bloquear a thread principal.
Você pode escolher por exemplo mandar um Web Worker executar parte da sua aplicação e deixar a principal apenas renderizar o DOM, ou então demandar para ele gerar um item para você como um arquivo Excel por exemplo. Isso permitiria deixar o browser livre para executar a parte principal da sua aplicação, não deixando a tela branca enquanto processa algo.

Para saber como criar um Web Worker no Angular: https://angular.io/guide/web-worker

Change Detection

A cada evento assíncrono, o Angular vai executar uma detecção de mudanças na árvore inteira de componentes, por exemplo:

Na imagem acima, se o componente Parent for alterado, toda a árvore detectará a mudança, pois são filhos de Parent, com isso todos os componentes renderizarão novamente no DOM e irão dar bind em todas as variáveis também.

Isso se chama Change Detection Strategy no Angular e esse é o comportamento default que ajuda muito para não termos que ficar pensando se queremos ou não atualizar a árvore de componentes. Mas, digamos que nessa árvore, o componente Comp 3 é muito oneroso e pesado para ser atualizado a cada mudança na árvore dele.

O que você pode fazer é mudar esse componente para ser atualizado apenas quando você precisa, manualmente. Para desativar o Change Detection do componente do default, você vai precisar alterar ele para OnPush:

A configuração do OnPush agora está dizendo que você vai passar a controlar a renderização desse componente, então apenas dentro da lógica que você possui, você pode acionar a renderização diretamente (há vários modos, como chamar um método tick() de um ComponentRef por exemplo).

trackBy (opção do *ngFor)

Por padrão o *ngFor identifica a unicidade de um objeto pela sua referência, por isso, quando o desenvolvedor adiciona um item programaticamente na lista que o ngFor está iterando o Angular não consegue saber exatamente qual foi o item adicionado no array, então ele simplesmente remove todos os itens do DOM e adiciona a lista atual com o item incluído.

Para evitar isso, você pode dizer para o Angular como indexar a sua lista de objetos, permitindo assim o Angular apenas renderizar o que foi mudado.

A sintaxe do seu ngFor ficaria assim:

E o Typescript:

Essa técnica é perfeita quando se tem uma lista no seu HTML que é atualizada manualmente no front-end, não quando todos os itens vêm de uma API e você vai adicionar todos de uma vez.

Para saber mais veja: https://angular.io/guide/built-in-directives#ngfor-with-trackby

Performance da memória

Por último e não menos importante, a performance da máquina (ou celular) do usuário.

Conforme ele utiliza a sua aplicação, está dando a impressão de que o browser ou a própria máquina está ficando cada vez mais lento? Pode ser que a sua aplicação está usando cada vez mais e mais memória e você nem sabe (quem já viu o Chrome usando praticamente toda a memória de uma máquina sabe do que estou falando).
Aqui vou dar duas dicas interessantes: dê unsubscribe nos seus Observables e evite usar o addEventListener puro do JavaScript.

Unsubscribe nos Observables

Quando você dá um .subscribre() em um Observable/Subject/BehaviorSubject, você cria uma subscription na memória, que fica lá armazenada para sempre avisar quando algum evento ocorrer e poder transmitir a mensagem.

Porém, depois que você muda de um componente para o outro no browser, o Angular destrói o apenas o componente, mas não destrói as subscriptions da memória. Você precisa fazer isso manualmente: sempre que um componente for destruído, remova as subscriptions que nele havia.

Existem diversas formas de se remover essas subscriptions. As principais formas, utilizando o RxJs, são com:

● takeUntil – a mais recomendada.
● takeWhile.
● unsubscribe.

Obs: O pipe AsyncPipe também faz um subscribe diretamente no HTML, porém, quando um componente é destruído esse já faz unsubscribe automaticamente =)
O método takeUntil é o mais recomendado por manter um código limpo e simples de replicar. Tudo o que você vai precisar fazer é incluir nos seus subscribes:

Você pode ver que no exemplo acima apenas precisei de uma variável para armazenar a referência de todos os subscribes para só no ngOnDestroy do componente chamá-lo e completá-lo, matando assim todas as subscriptions da memória.

Evite usar addEventListener

Sempre que você cria um listener no browser com o addEventListener, ele vai existir até que você chame um removeEventListener, segue a mesma lógica das subscriptions do RxJs.

A questão é que para esse caso você não precisa necessariamente ficar criando listeners com JavaScript puro, pois se criar os listeners usando as APIs do Angular, eles podem ser automaticamente removidos da memória para você ao destruir os componentes.

Existem diversas maneiras de utilizar o Angular para criar um listener, são elas:

EventBinding - Técnica que coloca o listener diretamente no HTML, como por exemplo:

ou:

HostListener - O jeito mais simples que também já vai remover o listener assim que o componente for destruído, exemplo:


Renderer2 - A classe Renderer2 oferece um método chamado listen que retorna uma função, que se invocada remove o listener da memória. É muito importante quando a sua interação depende da inserção e remoção dos listeners em tempo real, não apenas quando um componente é destruído.

Bem comum nos casos em que se vai implementar um Drag and Drop e se é necessário remover um listener ao soltar o objeto sendo arrastado.

Conclusão



Ao final desse artigo provavelmente a sua aplicação vai voar =o

Existem muitas e muitas técnicas ainda que podem ser aplicadas.

As técnicas listadas aqui são as mais importantes quando falamos de aplicações Angular (algumas que inclusive podem ser usadas para outras tecnologias SPA como Vue ou React).

Então lembre-se delas no seu próximo code review ou quando for criar a sua próxima aplicação web =)

Um grande abraço e até a próxima

Álvaro Junqueira: https://github.com/alvarocjunq

⚠️
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.Jueves, 5 de enero