O Problema
Lembra daquele app que começou rápido e depois foi ficando mais lento conforme adicionávamos features? Pois é, o culpado quase sempre é o mesmo: change detection.
O Angular, por padrão, é um pouco paranóico. A cada evento (scroll, clique, timer), ele verifica TODA a árvore de componentes pra ver se algo mudou.
Traduzindo: se você tem 100 componentes e 100 eventos por segundo, são 10.000 verificações por segundo. Não tem CPU que aguente!
Para resolver, listei abaixo 5 técnicas para melhorar a performance do seu app Angular. Algumas delas, mesmo sendo velhas conhecidas, ainda hoje costumam ser ignoradas.
1. OnPush - A Estratégia Mais Importante
Compatibilidade: Angular 2+
Impacto: Reduz change detection em ~90%
Essa aqui muda o jogo. O OnPush faz o Angular ser mais inteligente - ele só verifica mudanças quando realmente precisa.
@Component({
selector: 'app-user-card',
template: `<div>{{ user.name }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush // 👈 Essa mágica aqui
})
export class UserCardComponent {
@Input() user: User;
}
A pegadinha: Com OnPush, você precisa parar de mutar objetos:
// ❌ Isso quebra tudo (o Angular não detecta)
this.user.name = 'João';
// ✅ Isso funciona perfeitamente
this.user = { ...this.user, name: 'João' };
Recomendação: Todo componente novo já deveria nascer com OnPush. Só remove quando realmente não tem jeito (e isso é raro).
2. Signals - O Futuro Que Já Chegou
Compatibilidade: Angular 17+
Impacto: Mais eficiente que RxJS
Signals são mais simples que RxJS e mais eficientes. Depois de testá-las, viram a primeira escolha para muitos devs.
@Component({
selector: 'app-counter',
template: `
<div>Count: {{ count() }}</div> <!-- Note os () -->
<div>Doubled: {{ doubled() }}</div>
`
})
export class CounterComponent {
count = signal(0); // 👈 Declaração simples
doubled = computed(() => this.count() * 2); // 👈 Computed elegante
}
Por que Signals sobre RxJS:
// ❌ RxJS (funciona, mas é mais verboso)
count$ = new BehaviorSubject(0);
doubled$ = this.count$.pipe(map(c => c * 2));
// ✅ Signals (mais simples e eficiente)
count = signal(0);
doubled = computed(() => this.count() * 2);
A grande vantagem: com Signals, só os componentes que usam aquele signal específico são verificados. É muito mais granular.
Nota pra quem está em Angular 12-16: RxJS + OnPush funciona muito bem também!
3. Lazy Loading - Dividir para Conquistar
Compatibilidade: Angular 14+
Impacto: Isola componentes pesados
Componentes pesados ou telas menos acessadas devem ser lazy-loaded:
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent) // 👈 Só carrega quando acessado
}
];
Resultado: O app principal fica leve e rápido. O dashboard só carrega quando o usuário realmente precisa.
4. Unsubscribe - Evitando Vazamentos
Padrão recomendado (Clássico): takeUntil com Subject
Padrão moderno (Angular 16+): takeUntilDestroyed
Problema que resolve: Memory leaks e change detection desnecessário
Abordagem Clássica (Angular 2+)
export class DataComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); // 👈 Subject de limpeza
ngOnInit() {
this.dataService.getData()
.pipe(
takeUntil(this.destroy$) // 👈 Limpa automaticamente
)
.subscribe(data => {
this.data = data;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Abordagem Moderna (Angular 16+)
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-data',
template: `<div>{{ data }}</div>`
})
export class DataComponent implements OnInit {
private destroyRef = inject(DestroyRef);
data: any;
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.getData()
.pipe(
takeUntilDestroyed(this.destroyRef) // 👈 Muito mais simples!
)
.subscribe(data => {
this.data = data;
});
}
}
Por que funciona: Observables que continuam emitindo depois que o componente foi destruído são uma fonte comum de problemas. Esse padrão elimina o problema.
Qual usar? Se você está em Angular 16+, use takeUntilDestroyed. É mais limpo e não precisa implementar OnDestroy.
5. trackBy - O Segredo das Listas Rápidas
Compatibilidade: Angular 2+
Quando usar: Sempre em *ngFor com listas dinâmicas
Melhoria: Listas com 1000+ itens ficam fluidas
Abordagem Clássica (Angular 2+)
@Component({
template: `
<div *ngFor="let user of users; trackBy: trackByFn">
{{ user.name }}
</div>`
})
export class UserListComponent {
users: User[] = [];
// 👈 Essa função simples faz mágica
trackByFn(index: number, item: User) {
return item.id; // O Angular usa isso pra saber o que mudou
}
}
Abordagem Moderna com Control Flow (Angular 17+)
@Component({
template: `
@for (user of users; track user.id) {
<div>{{ user.name }}</div>
}
`
})
export class UserListComponent {
users: User[] = [];
// 👈 Não precisa de trackByFn! O 'track' é obrigatório
}
Antes: A lista inteira era re-renderizada a cada mudança
Depois: Só os itens que realmente mudaram são atualizados
Nota importante: No Angular 17+, o track é obrigatório na nova sintaxe de control flow (@for). Isso força boas práticas de performance desde o início.
Checklist de Performance
Antes de entregar qualquer feature, é legal verificar:
- Todos componentes usando OnPush?
- Signals implementados (se Angular 17+)?
- Componentes pesados em lazy loading?
- Observables com takeUntil ou takeUntilDestroyed?
- O trackBy em todas as listas (ou @for com track)?
- Testei no Performance Profiler?
Conclusão Prática
Comece com OnPush em tudo. Depois adote Signals onde possível (Angular 17+). Finalmente, mantenha um processo consistente de lazy loading + unsubscribe + trackBy.
Se você está em Angular 17+, aproveite as novas features: takeUntilDestroyed e control flow com @for. Elas tornam o código mais limpo e forçam boas práticas de performance.
Os resultados? Apps que mantêm a performance não importa quantas features adicionemos.
Just Code It!


Top comments (2)
4 and 5 are already outdated as we now use takeUntilDestroyed and 'track' with control flow
I had already made a note about
track, but I'll clarify it further and add a point abouttakeUntilDestroyedfor those using Angular 17+ (which hasn't yet been widely adopted by Brazilian companies).Thank you for your contribution. ^