Angular Change Detection & Performance
Understanding and optimizing change detection is crucial for building performant Angular applications. Angular uses Zone.js to detect changes and update the view.
How Change Detection Works
Change detection is the mechanism Angular uses to keep the component tree in sync with the data model. When an event occurs (user input, HTTP response, timer), Angular checks if any data changed and updates the DOM if necessary.
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
// Default strategy - checks entire tree
@Component({
selector: 'app-default',
template: `
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
`
// changeDetection: ChangeDetectionStrategy.Default (default)
})
export class DefaultComponent {
title = 'Default Strategy';
count = 0;
increment() {
this.count++;
// Angular automatically detects this change
}
}
// OnPush strategy - only checks when inputs change or events occur
@Component({
selector: 'app-onpush',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
<button (click)="updateAsync()">Update Async</button>
`
})
export class OnPushComponent {
@Input() data: any;
title = 'OnPush Strategy';
count = 0;
constructor(private cdr: ChangeDetectorRef) {}
// Button click triggers change detection
increment() {
this.count++;
}
// Async operation - need to manually trigger
updateAsync() {
setTimeout(() => {
this.count++;
this.cdr.markForCheck(); // Mark for check
}, 1000);
}
// Or detect changes immediately
forceUpdate() {
this.count++;
this.cdr.detectChanges(); // Run change detection now
}
}Change Detection Strategies
// Default: Checks component and all children on every change
@Component({
changeDetection: ChangeDetectionStrategy.Default
})
// OnPush: Only checks when:
// 1. Input reference changes
// 2. Event handler fires
// 3. Observable emits (with async pipe)
// 4. Manually triggered (markForCheck/detectChanges)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
// Example: OnPush component
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`
})
export class UserCardComponent {
@Input() user!: User;
// ❌ Won't trigger change detection
updateWrong() {
this.user.name = 'New Name';
// Mutating input property - reference stays same
}
// ✅ Will trigger change detection
updateCorrect() {
this.user = { ...this.user, name: 'New Name' };
// New reference created
}
}
// Parent component
@Component({
template: `<app-user-card [user]="currentUser"></app-user-card>`
})
export class ParentComponent {
currentUser = { name: 'John', email: 'john@example.com' };
// ❌ Won't trigger child change detection
updateWrong() {
this.currentUser.name = 'Jane';
}
// ✅ Will trigger child change detection
updateCorrect() {
this.currentUser = { ...this.currentUser, name: 'Jane' };
}
}Manual Change Detection Control
import { Component, ChangeDetectorRef, ApplicationRef } from '@angular/core';
@Component({
selector: 'app-manual',
template: `<p>{{ count }}</p>`
})
export class ManualComponent {
count = 0;
constructor(
private cdr: ChangeDetectorRef,
private appRef: ApplicationRef
) {}
// Mark component and ancestors for check
markForCheck() {
this.count++;
this.cdr.markForCheck();
}
// Run change detection immediately for this component
detectChanges() {
this.count++;
this.cdr.detectChanges();
}
// Detach from change detection tree
detach() {
this.cdr.detach();
// Component no longer checked automatically
}
// Reattach to change detection tree
reattach() {
this.cdr.reattach();
}
// Run change detection for entire application
triggerAppCheck() {
this.appRef.tick();
}
}Performance Optimization Techniques
TrackBy Function
@Component({
template: `
<!-- Without trackBy - recreates all DOM nodes on array change -->
<div *ngFor="let item of items">
{{ item.name }}
</div>
<!-- With trackBy - only updates changed items -->
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
`
})
export class ListComponent {
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
];
// TrackBy function
trackByFn(index: number, item: any): any {
return item.id; // Unique identifier
}
// Or track by index
trackByIndex(index: number): number {
return index;
}
}Pure Pipes for Expensive Operations
// Pure pipe - cached result
@Pipe({
name: 'expensiveFilter',
pure: true // default
})
export class ExpensiveFilterPipe implements PipeTransform {
transform(items: any[], searchText: string): any[] {
console.log('Pipe running'); // Logs only when inputs change
return items.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
}
}
// Usage
<div *ngFor="let item of items | expensiveFilter:searchText">
{{ item.name }}
</div>Lazy Loading & Code Splitting
// Lazy load modules
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'users',
loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
}
];
// Preloading strategies
import { PreloadAllModules, RouterModule } from '@angular/router';
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules // Preload all lazy routes
});Virtual Scrolling
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
standalone: true,
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" style="height: 500px">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
}Zone.js and NgZone
import { Component, NgZone } from '@angular/core';
@Component({
selector: 'app-zone-demo'
})
export class ZoneDemoComponent {
constructor(private ngZone: NgZone) {}
// Run outside Angular zone - no change detection
performHeavyTask() {
this.ngZone.runOutsideAngular(() => {
// Heavy computation or animation
setInterval(() => {
// This won't trigger change detection
console.log('Running outside zone');
}, 100);
});
}
// Run inside Angular zone - triggers change detection
updateUI() {
this.ngZone.run(() => {
// Update component data
this.count++;
});
}
// Handle events outside zone
setupEventListener() {
this.ngZone.runOutsideAngular(() => {
document.addEventListener('mousemove', (event) => {
// High-frequency event - outside zone for performance
// Update when needed
if (/* some condition */) {
this.ngZone.run(() => {
// Update component state
});
}
});
});
}
}Performance Best Practices
Use OnPush strategy for components with input properties
Use trackBy for *ngFor to avoid unnecessary DOM updates
Use pure pipes for expensive transformations
Lazy load modules and use code splitting
Use virtual scrolling for large lists
Run heavy computations outside Angular zone
Use async pipe for observables (auto unsubscribe)
Detach from change detection for static content
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free