After mastering Default vs OnPush strategies, I hit a wall with a real-time stock ticker dashboard. Even with OnPush everywhere, certain components lagged during market hours. That's when I dove into ChangeDetectorRef APIs and uncovered common performance traps. This article shares the exact techniques that took my app from 2.3s to 180ms load times.
ChangeDetectorRef: Fine-Grained Control
ChangeDetectorRef gives you manual control over when Angular checks specific components. Import it and inject into OnPush components:
import { ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StockTickerComponent {
constructor(private cdr: ChangeDetectorRef) {}
}
The Four Essential Methods
1. markForCheck() – The OnPush Hero
Marks this component + ancestors for the next change detection cycle.
// Perfect for async callbacks in OnPush components
fetchStockData() {
this.stockService.getPrice().subscribe(price => {
this.currentPrice = price;
this.cdr.markForCheck(); // Triggers next CD cycle
});
}
When to use: Observables, timers, Promises in OnPush components.
2. detectChanges() – Run CD Right Now
Immediately runs change detection on this component + children.
// For third-party widgets or canvas updates
updateChart() {
this.chart.updateData(this.data);
this.cdr.detectChanges(); // DOM updates instantly
}
Caution: Can cause infinite loops if overused.
3. detach() / reattach() – Complete Opt-Out
Removes component from automatic change detection entirely.
ngOnInit() {
if (this.isStaticContent) {
this.cdr.detach(); // Never checked automatically
}
}
updateDynamicContent() {
this.cdr.reattach(); // Back in the game
this.cdr.detectChanges();
}
Use case: Static banners, readonly displays, heavy animations.
Common Performance Killers (And Their Fixes)
1. Template Computations – The Silent Killer
Heavy work in templates (runs every CD cycle)
<div>{{ expensiveFilter(users) }}</div>
Move to component or use pure pipes
@Pipe({ pure: true })
export class FilterPipe { transform(users: User[]) { ... } }
2. ngFor Without trackBy – List Re-Render Hell
Destroys/recreates ALL DOM nodes on array changes
<div *ngFor="let user of users">{{ user.name }}</div>
Only updates changed items
<div *ngFor="let user of users; trackBy: trackByUserId">{{ user.name }}</div>
trackByUserId(index: number, user: User): number {
return user.id; // Stable identifier
}
Impact: 1000 items → 85% render time reduction.
3. Mutable State in OnPush – The Gotcha
Won't trigger OnPush (same reference!)
this.users[0].name = 'Updated';
Creates new reference → triggers OnPush
this.users = this.users.map((u, i) =>
i === 0 ? { ...u, name: 'Updated' } : u
);
My Performance Optimization Checklist
1. OnPush on all presentational/list components
2. trackBy on every *ngFor
3. Pure pipes for filtering/sorting
4. Immutable updates (spread operator, map/filter)
5. async pipe over manual subscribe()
6. PurePipe for expensive template logic
7. Lazy load feature modules
8. Signals for local component state (Angular 17+)
Real-World Example: The Laggy Dashboard Fix
Before (2.3s render):
@Component({ /* Default strategy */ })
export class DashboardComponent {
users = []; // Mutable array
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users; // Direct assignment
});
}
}
Template: <div *ngFor="let user of filterUsers(users)">{{ user.name }}</div>
After (180ms render):
@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
export class DashboardComponent {
users = signal<User[]>([]);
filteredUsers = computed(() =>
this.users().filter(u => u.active)
);
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users.set(users); // Immutable signal update
// No cdr.markForCheck() needed with signals!
});
}
}
Template: <div *ngFor="let user of filteredUsers(); trackBy: trackById">
Benchmark Results (Chrome Performance Tab)
Component Tree: 120 components
Default: 2,347ms per update
OnPush: 892ms per update (62% improvement)
OnPush+trackBy: 428ms per update
Signals+OnPush: 182ms per update (92% total improvement!)
When Signals Replace ChangeDetectorRef (Angular 17+)
Signals often eliminate manual markForCheck() calls entirely:
// No cdr needed! Template reads trigger automatic CD
readonly users = signal<User[]>([]);
readonly filteredUsers = computed(() => this.users().filter(u => u.active));
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users.set(users); // Auto-triggers template update
});
}
Migration path: OnPush → Signals → Zoneless (future).
Next: Signals vs traditional change detection + real-world debugging scenarios that trip up every Angular developer.
