← Back to list

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…

Satnam Singh · 2025-11-04 05:36 · 161 claps · 2.8 min read
#angular #performance #front-end-development #javascript #angular-tips
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🔧 · Data Engineering

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