Angular Change Detection
Introduction
Change Detection is one of Angular's core features that determines when and how the UI should be updated to reflect changes in your application's data. Understanding how it works is crucial for building performant Angular applications, especially as they grow in complexity.
In this guide, we'll explore Angular's Change Detection mechanism, how it works under the hood, and techniques to optimize it for better performance.
What is Change Detection?
Change Detection is Angular's process of synchronizing your component's data model with its view. In simpler terms:
- When data changes in your application (e.g., through user interaction, HTTP responses, timers)
- Angular detects these changes
- Angular updates the DOM to reflect these changes
Unlike manual DOM manipulation in vanilla JavaScript, Angular handles this process automatically, which is one of its key benefits.
How Change Detection Works in Angular
Angular's Change Detection is powered by a library called Zone.js. Let's break down how it works:
Zone.js and NgZone
import { NgZone } from '@angular/core';
@Component({
selector: 'app-example',
template: `<h1>{{ data.count }}</h1>
<button (click)="increment()">Increment</button>`
})
export class ExampleComponent {
data = { count: 0 };
constructor(private zone: NgZone) {}
increment() {
this.data.count++;
// Angular automatically detects this change and updates the view
}
}
Zone.js is a library that intercepts and keeps track of all asynchronous operations in your application. Angular uses a specific zone called NgZone to trigger change detection when:
- Events (click, input, submit, etc.)
- AJAX requests (HTTP)
- Timers (setTimeout, setInterval)
- Promises or Observables resolving
When any of these operations complete, Angular knows it should check if any data has changed and update the view accordingly.
The Change Detection Tree
Angular organizes components in a hierarchical tree structure. By default, whenever change detection is triggered, Angular checks each component in the tree from top to bottom to see if any bindings have changed.
AppComponent
├── HeaderComponent
├── SidebarComponent
└── ContentComponent
├── ArticleComponent
└── CommentsComponent
When a change occurs, Angular starts at the root (AppComponent
) and traverses down the entire tree, checking each component along the way.
Change Detection Strategies
Angular provides two change detection strategies:
Default (CheckAlways)
By default, Angular checks the entire component tree whenever any change is detected:
@Component({
selector: 'app-default-example',
template: `<h1>{{ title }}</h1>`,
changeDetection: ChangeDetectionStrategy.Default // This is the default
})
export class DefaultExampleComponent {
title = 'Default Strategy';
}
OnPush
The OnPush strategy makes Angular only check a component when:
- Input properties change (references, not values)
- An event originated from the component or its children
- Explicitly telling Angular to check with
ChangeDetectorRef
- Using the async pipe in the template
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-onpush-example',
template: `
<h2>{{ data.name }}</h2>
<p>Count: {{ data.count }}</p>
<button (click)="updateData()">Update Data (Won't Work)</button>
<button (click)="updateDataImmutably()">Update Data Immutably</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushExampleComponent {
@Input() data: { name: string, count: number };
updateData() {
// This won't trigger change detection with OnPush strategy
this.data.count++;
}
updateDataImmutably() {
// This will trigger change detection with OnPush strategy
this.data = { ...this.data, count: this.data.count + 1 };
}
}
Optimizing Change Detection
Let's explore some practical ways to optimize change detection in your Angular applications:
1. Using OnPush Strategy
The first step is to apply the OnPush strategy to components that don't need to update frequently:
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-pure-component',
template: `<div>{{ heavyComputation() }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PureComponent {
@Input() data: any;
heavyComputation() {
console.log('Heavy computation executed');
// Some expensive operation
return 'Result';
}
}
2. Manual Change Detection with ChangeDetectorRef
Sometimes you need to manually control when change detection runs:
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
@Component({
selector: 'app-manual-detection',
template: `<h3>{{ value }}</h3>
<button (click)="updateValueAndDetect()">Update</button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualDetectionComponent {
value = 'Initial';
constructor(private cd: ChangeDetectorRef) {
// Detaches the component from change detection
// this.cd.detach();
}
updateValueAndDetect() {
this.value = 'Updated: ' + new Date().toLocaleTimeString();
// Manually tell Angular to detect changes
this.cd.detectChanges();
// Alternative: mark for check (will be checked in the next CD cycle)
// this.cd.markForCheck();
}
}
3. Running Outside NgZone
For performance-sensitive operations, you can run code outside Angular's zone to avoid triggering change detection:
import { Component, NgZone } from '@angular/core';
@Component({
selector: 'app-zone-example',
template: `<h3>Progress: {{ progress }}</h3>`
})
export class ZoneExampleComponent {
progress = 0;
constructor(private ngZone: NgZone) {}
startHeavyOperation() {
// Run outside Angular's zone to avoid triggering change detection
this.ngZone.runOutsideAngular(() => {
let interval = setInterval(() => {
this.progress++;
if (this.progress === 100) {
clearInterval(interval);
// When done, re-enter Angular's zone to update the view
this.ngZone.run(() => {
console.log('Operation completed, updating view');
});
}
}, 20);
});
}
}
4. Using the Async Pipe
The async pipe automatically subscribes to observables and updates the view when new values arrive, working efficiently with OnPush:
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map, take } from 'rxjs/operators';
@Component({
selector: 'app-async-example',
template: `
<div>Time: {{ timer$ | async }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AsyncExampleComponent {
timer$ = interval(1000).pipe(
take(10),
map(i => new Date().toLocaleTimeString())
);
}
Common Change Detection Issues and Solutions
Issue 1: Mutable Objects with OnPush
Problem:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductComponent {
@Input() product: Product;
updateQuantity() {
// This won't trigger change detection with OnPush
this.product.quantity++;
}
}
Solution:
updateQuantity() {
// Create a new reference to trigger change detection with OnPush
this.product = { ...this.product, quantity: this.product.quantity + 1 };
}
Issue 2: Excessive Change Detection in Lists
Problem: When rendering large lists, change detection can become expensive.
Solution: Use trackBy
to help Angular identify which items changed:
@Component({
selector: 'app-user-list',
template: `
<div *ngFor="let user of users; trackBy: trackByUserId">
{{ user.name }}
</div>
`
})
export class UserListComponent {
users: User[] = [];
trackByUserId(index: number, user: User): number {
return user.id;
}
}
Real-World Example: Optimizing a Dashboard
Let's imagine a dashboard application with multiple widgets displaying data from different sources:
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<app-widget
*ngFor="let widget of widgets"
[config]="widget"
[data]="widgetData[widget.id] | async">
</app-widget>
</div>
<button (click)="refreshAllData()">Refresh All</button>
`,
})
export class DashboardComponent {
widgets = [
{ id: 'sales', title: 'Sales Overview', refreshInterval: 60000 },
{ id: 'users', title: 'Active Users', refreshInterval: 30000 },
{ id: 'performance', title: 'System Performance', refreshInterval: 5000 }
];
widgetData: { [key: string]: Observable<any> } = {};
constructor(private dataService: DataService) {
// Initialize each widget with its data stream
this.widgets.forEach(widget => {
this.widgetData[widget.id] = this.dataService.getDataStream(widget.id, widget.refreshInterval);
});
}
refreshAllData() {
this.widgets.forEach(widget => {
this.dataService.refreshData(widget.id);
});
}
}
@Component({
selector: 'app-widget',
template: `
<div class="widget">
<h3>{{ config.title }}</h3>
<div *ngIf="data; else loading">
<!-- Widget content -->
<div class="widget-value">{{ data.value }}</div>
<div class="widget-chart" *ngIf="data.history">
<!-- Chart visualization -->
</div>
</div>
<ng-template #loading>
<div class="loading">Loading...</div>
</ng-template>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetComponent {
@Input() config: any;
@Input() data: any;
}
This example demonstrates:
- Using OnPush for widget components that only update when their data changes
- Using the async pipe to efficiently handle observable data streams
- Isolating components so they only re-render when their specific data changes
Summary
Angular's Change Detection is a powerful system that automatically synchronizes your application's data with the UI. By understanding how it works and applying optimization techniques, you can significantly improve the performance of your Angular applications.
Key takeaways:
- Change Detection is Angular's mechanism for updating the DOM when data changes
- Angular uses Zone.js to automatically detect when to run change detection
- The OnPush strategy can significantly improve performance by reducing unnecessary checks
- Immutable data patterns work best with OnPush strategy
- Tools like ChangeDetectorRef, NgZone, and the async pipe help you optimize change detection
Additional Resources
- Angular Official Documentation on Change Detection
- Maximizing Performance with OnPush
- Understanding Zone.js
Exercises
- Convert a component using default change detection to use OnPush and ensure it still works correctly.
- Implement a counter component that uses
runOutsideAngular
for the timer and only updates the view every second. - Create a list component that efficiently renders 1000+ items using trackBy and OnPush strategy.
- Build a simple dashboard with multiple independent widgets, each with its own change detection optimization.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)