Do You Make These 10 Angular Performance Mistakes That Keep Your App Slow?
Source: Dev.to
Overview
Imagine your Angular app lagging at peak usage, frustrating users and tanking metrics. Outdated habits like default change detection and *ngFor are often the culprits, but Angular 17+ counters them with signals, zoneless apps, @for/@if control‑flow, and Angular 21’s experimental Signal Forms for cleaner reactivity.
This guide tackles the top 10 performance mistakes with Angular 21+ fixes, featuring code snippets using @for/@if and signal‑based form(), plus tools like Angular DevTools. Perfect for beginners debugging slowdowns, pros scaling with signals, and stakeholders seeing why modern Angular drives retention.
1️⃣ Default Change Detection
Default change detection in Angular triggers globally on every event via Zone.js, scanning the entire component tree and wasting CPU cycles in complex apps with deep hierarchies.
Fix:
- Use signals together with the OnPush strategy.
- Signals provide fine‑grained, zoneless updates that target only the changed sub‑trees, slashing unnecessary checks.
This shift from broad sweeps to precise reactivity transforms performance in large‑scale applications.
2️⃣ OnPush & Immutable Data
Relying on the default strategy checks every component on every cycle, even unchanged ones, bloating runtime overhead.
OnPush skips untouched sub‑trees unless inputs mutate or events fire, but it demands immutable data – mutating objects silently fails because Angular uses reference equality (Object.is()).
Pair it with signals for automatic dirty marking on input changes—no manual tweaks needed.
3️⃣ Manual Subscriptions vs. Async Pipe
Manual
.subscribe()in components creates extra state variables that trigger full detection cycles, piling on memory leaks and redundant checks.
Fix:
- Use the async pipe – it auto‑subscribes/unsubscribes, marks OnPush components dirty only on emissions, and integrates seamlessly with
toSignal()for reactive streams without Zone.js monkey‑patching.
Ditch manual subscriptions; let pipes handle the heavy lifting for leaner code.
4️⃣ Computed Signals vs. Template Watchers
Complex templates with nested property chains spawn watchers that re‑evaluate constantly, hammering performance during global runs.
Fix:
- Use computed signals to derive state lazily—recalculating only when dependencies shift—eliminating excess watchers while caching results for speed.
Memoize filtered lists, totals, etc., keeping templates crisp.
5️⃣ Example: Refactor a User List Component
❌ Before – Default Change Detection & Manual State
@Component({ changeDetection: ChangeDetectionStrategy.Default })
export class UserList {
protected users: User[] = [];
protected loading = false;
ngOnInit() {
this.loading = true;
this.userService.getUsers().pipe(
take(1),
tap((users) => this.users = users),
finalize(() => this.loading.set(false))
).subscribe();
}
}
✅ After – OnPush + Signals + @for/@if
import { ChangeDetectionStrategy, Component, computed, signal, input } from '@angular/core';
import { take, tap, finalize } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (loading()) { Loading... }
@for (user of filteredUsers(); track user.id) {
- {{ user.name }}
}
`
})
export class UserList {
/** Signal inputs auto‑track */
readonly users = input([]);
readonly filter = input('');
protected loading = signal(false);
protected filteredUsers = computed(() =>
this.users().filter(u => u.name.includes(this.filter()))
);
constructor(private userService: UserService) {
// Start loading immediately before subscription
this.loading.set(true);
this.userService.getUsers().pipe(
take(1), // Prevent unnecessary work if source is chatty
tap((users) => this.users.set(users)),
finalize(() => this.loading.set(false))
).subscribe();
}
}
Result: DevTools profiler shows 70 %+ fewer checks—only the list updates on filter tweaks or data loads. Signals + OnPush crush wasteful cycles, making the app feel snappier.
6️⃣ Dynamic Lists – @for vs. *ngFor
Dynamic lists often suffer from full DOM recreation on every update when using outdated
*ngForwithout proper tracking, leading to sluggish performance on large datasets.
- Angular 17 introduced the
@forsyntax, which mandates a track expression (think of it as a built‑in guardrail against sloppy loops). - Developers still cling to
*ngForwithouttrackBy, causing Angular to tear down and rebuild entire lists even for minor changes like adding one item. Benchmarks show operations on 100 + items jumping from milliseconds to seconds without tracking.
Fix:
@for (item of items; track item.id) {
- {{ item.name }}
}
- No imports needed. Community tests report ~90 % faster diffing on large lists.
7️⃣ Bundle Bloat – Tree Shaking & Budgets
- Importing whole libraries (e.g., Moment.js) without tree‑shaking balloons
main.jspast 500 KB, delaying initial loads—especially painful on mobile. - Angular’s esbuild (default since v17) aggressively shakes dead code, but you must enable production budgets in
angular.jsonto catch bloat early:
{
"projects": {
"your-app": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
}
}
}
}
}
- Gzip or Brotli compresses bundles by 80 %, dropping a 2 MB file to a much smaller size.
- Lazy‑load heavy modules to keep the initial bundle lean:
import('./admin/admin.component').then(m => m.AdminComponent);
- This cuts load times by 50‑70 % for enterprise apps.
- Run
ng generate @angular/core:control-flowto migrate loops, then analyze bundles with source‑map‑explorer for quick wins.
9️⃣ Quick Demo – *ngFor → @for
Replace
*ngFor="let item of items; trackBy: trackById"with@for (item of items; track item.id)on a 1 000‑item list.
- DevTools profiler shows re‑renders dropping from 500 ms+ to under 50 ms.
- Angular now smartly appends/updates only the changed items.
🔟 Uncleaned RxJS Subscriptions
Uncleaned RxJS subscriptions create memory leaks in single‑page applications by keeping destroyed components alive in memory, leading to performance degradation over time.
- Solution: Prefer the async pipe (or
toSignal) which auto‑unsubscribes, or manually manage subscriptions withtakeUntilDestroyed(Angular 17+).
📈 Takeaway
- Signals + OnPush = fine‑grained, zoneless reactivity.
@for/@if= built‑in, high‑performance control flow.- Async pipe /
toSignal= safe, leak‑free subscriptions. - Computed signals = lazy, memoized state.
- Lazy loading, budgets, and esbuild = keep bundles lean.
Adopt these patterns, and your Angular app will feel snappier, more scalable, and ready for the future—delighting users and impressing stakeholders. 🚀
📉 Observable Cleanup & Memory Leaks
Angular 16+ introduced takeUntilDestroyed() to automatically unsubscribe observables when a component is destroyed, eliminating most manual cleanup. Signals further reduce subscription reliance, and Angular v21+ experimental Signal Forms can remove observable usage entirely.
- Typical problem – forgetting to unsubscribe forces manual
ngOnDestroylogic, which is error‑prone (especially with higher‑order operators likemergeMap). - Modern fixes
// RxJS pipe with automatic cleanup
this.dataService.getItems()
.pipe(takeUntilDestroyed())
.subscribe();
// Convert to a signal (auto‑unsubscribes on destroy)
items = toSignal(this.dataService.getItems());
toSignal()unsubscribes on component destruction.effect()handles side‑effects with built‑in cleanup.
📝 Forms & Change Detection
Heavy use of [(ngModel)] or classic reactive forms creates endless change‑detection cycles and observable chains for validation.
Switch to the experimental form() API (Angular 21):
import { form, signalControl } from '@angular/forms';
export class MyComponent {
form = form({
email: signalControl('') // type‑safe, signal‑based control
});
}
- Benefits: inferred type safety, simpler state management, no subscription boilerplate, and lower memory overhead in dashboards or complex UIs.
🌐 Direct DOM Access
Using document.querySelector() bypasses Angular’s change detection and breaks SSR/hydration safety.
Recommended approach
import { viewChild, effect, isPlatformBrowser } from '@angular/core';
import { ElementRef } from '@angular/common';
export class MyComponent {
element = viewChild('myElement');
constructor() {
effect(() => {
if (isPlatformBrowser && this.element()) {
this.element()!.nativeElement.focus();
}
});
}
}
viewChild()provides type‑safe, reactive access.- Pair with
isPlatformBrowser()for SSR‑safe queries.
🔀 RxJS vs. Signals for Derived State
Over‑engineering with RxJS pipes for derived state creates “stream tangles” that are hard to debug and memory‑intensive.
Prefer signals for UI state:
import { computed, effect } from '@angular/core';
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
// Side‑effect without a subscription
logEffect = effect(() => console.log('Count:', this.count()));
}
- Use RxJS for HTTP streams or complex event pipelines.
- Use signals for UI‑state derivations—faster, fine‑grained updates, no subscriptions required.
🚀 Migration Path
-
Wrap legacy observables
const items = toSignal(this.obs$, { initialValue: null });Combine with
takeUntilDestroyed()as a bridge. -
Adopt Signal Forms to eliminate
valueChangesobservables. -
Validate with Angular DevTools – spot memory leaks and verify that components are truly destroy‑clean.
⚡️ Angular 17+ Performance Boosts
- New
@forsyntax replaces legacy*ngFor. - OnPush + Signals for fine‑grained change detection.
@deferenables lazy loading of heavy components.- Signal Forms streamline form handling.
These improvements can cut load times by 50 – 90 % and dramatically improve Core Web Vitals.
Upgrade Steps
# Convert templates to the new control‑flow syntax
ng g @angular/core:control-flow
- Switch components to OnPush change detection.
- Adopt signals for reactive updates.
- Use
@deferfor lazy loading. - Migrate forms to Signal Forms.
Measure the impact with Chrome DevTools or Lighthouse.
📚 Further Reading
- Angular performance best practices
- Signals guide
- Zoneless change detection documentation
These resources show how to make Angular apps faster, leaner, and future‑ready while boosting Core Web Vitals.
📦 Tools & Templates for Your Business
I’ve compiled a collection of ready‑to‑use landing pages, automation starters, and templates that help you launch faster.
- ScaleSail.io – check out the toolkit.
- LinkedIn – follow me for actionable Angular & modern frontend insights.
- Twitter / X – stay updated with quick tips and real‑world project stories.
Your support fuels more practical guides and tools for the frontend and startup community. Let’s keep building together! 🚀