Skip to main content

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:

  1. When data changes in your application (e.g., through user interaction, HTTP responses, timers)
  2. Angular detects these changes
  3. 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

typescript
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:

typescript
@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
typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductComponent {
@Input() product: Product;

updateQuantity() {
// This won't trigger change detection with OnPush
this.product.quantity++;
}
}

Solution:

typescript
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:

typescript
@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:

typescript
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:

  1. Using OnPush for widget components that only update when their data changes
  2. Using the async pipe to efficiently handle observable data streams
  3. 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

Exercises

  1. Convert a component using default change detection to use OnPush and ensure it still works correctly.
  2. Implement a counter component that uses runOutsideAngular for the timer and only updates the view every second.
  3. Create a list component that efficiently renders 1000+ items using trackBy and OnPush strategy.
  4. 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! :)