9 Angular Anti-Patterns That Secretly Kill Performance
Angular is a robust framework, but even experienced developers can unintentionally introduce patterns that silently degrade application…
9 Angular Anti-Patterns That Secretly Kill Performance
Angular is a robust framework, but even experienced developers can unintentionally introduce patterns that silently degrade application performance. Some anti-patterns are subtle — they don’t throw errors, but they slow down rendering, increase memory usage, and make apps harder to maintain.
In this article, we’ll go through the most common Angular anti-patterns, explain why they’re harmful, and show better alternatives with examples. Angular is fast — until we make it slow.
Most performance issues in Angular apps don’t come from the framework itself, but from the way we use it.
Over the years, I’ve seen the same mistakes repeated in large teams: unnecessary re-renders, memory leaks, and wasted change detection cycles.
Let’s talk about 9 Angular anti-patterns that silently kill performance — and what you should do instead.
1. Forgetting OnPush Change Detection
Problem:
By default, Angular’s change detection checks everything in your component tree after every event.
If you don’t use ChangeDetectionStrategy.OnPush, the app will re-render more than necessary.
@Component({
selector: 'app-profile',
templateUrl: './profile.html',
})
export class ProfileComponent {} // ❌ Default (inefficient)
Fix:
@Component({
selector: 'app-profile',
templateUrl: './profile.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileComponent {} // ✅ Only updates on input or observable changes
Use OnPush by default — it’s the single easiest performance win.
2. Not Using trackBy in *ngFor
Problem: Angular re-renders the entire list when an item changes, even if only one element was updated.
<li *ngFor="let user of users">{{ user.name }}</li> <!-- ❌ -->
Fix:
<li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>
trackById(index: number, user: User) {
return user.id;
}
This prevents Angular from destroying and recreating DOM nodes unnecessarily.
3. Manual Subscriptions Without Cleanup
Problem: Subscribing manually in components without unsubscribing leads to memory leaks.
this.userService.getUser().subscribe(user => this.user = user); // ❌
Fix: Use async pipe or takeUntil pattern.
<p>{{ user$ | async }}</p> <!-- ✅ -->
Or if you must subscribe manually:
private destroy$ = new Subject<void>();
this.userService.getUser()
.pipe(takeUntil(this.destroy$))
.subscribe(user => this.user = user);
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
4. Overusing ngIf and ngFor Nesting
Problem:
Deeply nested *ngIf and *ngFor directives cause Angular to repeatedly create and destroy templates.
<div *ngIf="users.length">
<div *ngFor="let user of users">
<div *ngIf="user.active">
{{ user.name }}
</div>
</div>
</div>
Fix: Flatten logic with computed lists:
activeUsers$ = this.users$.pipe(
map(users => users.filter(u => u.active))
);
<div *ngFor="let user of activeUsers$ | async">
{{ user.name }}
</div>
5. Heavy Logic Inside Templates
Problem: Putting function calls or expressions directly in templates triggers re-evaluation on every change detection cycle.
<p>{{ getUserName(user) }}</p> <!-- ❌ -->
Fix: Pre-compute in the component or use pipes sparingly.
userName = this.userService.getUserName(user); // ✅
Templates should be declarative, not computational.
6. Recreating Observables in Templates
Problem: Creating observables inline in templates triggers new subscriptions each time.
<p>{{ getUser$() | async }}</p> <!-- ❌ -->
Fix: Assign the observable once in the component:
user$ = this.getUser$(); // ✅
<p>{{ user$ | async }}</p>
7. Ignoring Lazy Loading
Problem: Importing all features in the main module increases your initial bundle size.
@NgModule({
imports: [DashboardModule, ReportsModule, AdminModule] // ❌ all loaded upfront
})
export class AppModule {}
Fix: Use lazy loading routes.
const routes: Routes = [
{ path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) }
];
This reduces initial load time drastically.
8. Excessive Two-Way Binding
Problem:
Using [(ngModel)] everywhere leads to unnecessary DOM updates and sync overhead.
<input [(ngModel)]="formData.name"> <!-- ❌ -->
Fix: Use Reactive Forms instead.
form = this.fb.group({ name: [''] });
<input [formControl]="form.get('name')"> <!-- ✅ -->
Reactive Forms are more predictable and performant.
9. Not Leveraging trackBy or OnPush Together
These two aren’t just individual tips — they multiply each other’s effect.
Using trackBy without OnPush still causes unnecessary checks. Using OnPush without trackBy still recreates lists.
Always use them together for list-heavy UIs.
Bonus: Unoptimized Third-Party Components
Some UI libraries (or custom components) trigger deep re-renders or listen to global change detection.
If you see lag, check for components that don’t use OnPush internally. Wrap them inside an OnPush parent or isolate them in a child module.
Final Thoughts
Angular doesn’t need “performance hacks.” It needs discipline — knowing what not to do.
These anti-patterns are silent killers because they rarely show up until your app grows. Fix them early, and your Angular app will feel instant — no complex profiling needed.
메타데이터
- post_id
- 83c8df454569
- slug
- 9-angular-anti-patterns-that-secretly-kill-performance-83c8df454569
- url
- https://medium.com/@satnammca/9-angular-anti-patterns-that-secretly-kill-performance-83c8df454569
- canonical_url
- https://medium.com/@satnammca/9-angular-anti-patterns-that-secretly-kill-performance-83c8df454569
- author_url
- https://medium.com/@satnammca
- status
- ok
- fetched_at
- 2026-06-10 13:10:15