Skip to main content

OnPush Strategy

What is OnPush?

Change detection only runs when:

  1. Input reference changes
  2. Events in template
  3. Async pipe emits
  4. Manual markForCheck()
import { Component, ChangeDetectionStrategy, input } from '@angular/core';

@Component({
selector: 'app-user',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ user().name }}</p>`
})
export class UserComponent {
user = input.required<User>();
}

Input Reference Changes

// Parent component
@Component({
template: `<app-user [user]="currentUser"></app-user>`
})
export class ParentComponent {
currentUser = { name: 'John', age: 30 };

// ❌ Won't trigger change detection in child
updateUserWrong() {
this.currentUser.name = 'Jane';
}

// ✅ Will trigger change detection in child
updateUserCorrect() {
this.currentUser = { ...this.currentUser, name: 'Jane' };
}
}

Arrays and Objects

import { Component, ChangeDetectionStrategy, input } from '@angular/core';

@Component({
selector: 'app-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent {
// Signal-based input
items = input<Item[]>([]);

// Note: With signal inputs, the parent manages the array state
// The child receives updates when parent creates new reference
}

With Signals (Automatic)

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
`
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);

increment() {
this.count.update(v => v + 1); // Automatically triggers CD
}
}

With Async Pipe

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
})
export class UserListComponent {
users$ = this.userService.getUsers(); // Async pipe handles CD
}

Manual Change Detection

import { ChangeDetectorRef } from '@angular/core';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
cdr = inject(ChangeDetectorRef);
count = 0;

ngOnInit() {
// External event (not tracked by Angular)
setInterval(() => {
this.count++;
this.cdr.markForCheck(); // Manually trigger CD
}, 1000);
}

// Immediate detection
updateNow() {
this.count++;
this.cdr.detectChanges(); // Run CD immediately
}
}

Events in Template

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>{{ count }}</p>
<button (click)="increment()">+</button> <!-- Event triggers CD -->
`
})
export class CounterComponent {
count = 0;

increment() {
this.count++; // Works because triggered by template event
}

// ❌ Won't update view if called from outside
incrementFromOutside() {
this.count++; // No CD triggered
}
}

Observables with OnPush

@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataComponent {
cdr = inject(ChangeDetectorRef);
data: any[] = [];

ngOnInit() {
// ❌ Manual subscription needs markForCheck
this.dataService.getData().subscribe(data => {
this.data = data;
this.cdr.markForCheck(); // Required!
});

// ✅ Better: Use async pipe (no manual CD needed)
this.data$ = this.dataService.getData();
}
}

Immutability Helpers

// Array operations
const newArray = [...oldArray, newItem]; // Add
const newArray = oldArray.filter(item => item.id !== id); // Remove
const newArray = oldArray.map(item =>
item.id === id ? { ...item, name: 'New' } : item
); // Update

// Object operations
const newObj = { ...oldObj, name: 'New' }; // Update
const newObj = { ...oldObj, nested: { ...oldObj.nested, value: 'New' } }; // Deep update

// Using libraries
import { produce } from 'immer';
const newState = produce(state, draft => {
draft.user.name = 'Jane'; // Mutate draft, get immutable result
});

trackBy with OnPush

import { Component, ChangeDetectionStrategy, input } from '@angular/core';

@Component({
selector: 'app-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let item of items(); trackBy: trackById">
{{ item.name }}
</div>
`
})
export class ListComponent {
items = input<Item[]>([]);

trackById(index: number, item: Item): number {
return item.id; // Track by ID instead of reference
}
}

Nested Components

// Parent with OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<app-child [data]="data"></app-child>`
})
export class ParentComponent {
data = { value: 0 };

// ❌ Won't update child
updateWrong() {
this.data.value++;
}

// ✅ Will update child
updateCorrect() {
this.data = { ...this.data, value: this.data.value + 1 };
}
}

// Child with OnPush
@Component({
selector: 'app-child',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ data().value }}</p>`
})
export class ChildComponent {
data = input.required<{ value: number }>();
}

Performance Benefits

// Before (Default strategy)
@Component({
changeDetection: ChangeDetectionStrategy.Default
})
export class HeavyComponent {
// CD runs on EVERY change detection cycle
get expensiveComputation() {
return this.items.reduce(...); // Runs constantly!
}
}

// After (OnPush)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeavyComponent {
items = signal([...]);

// Only recalculates when items changes
total = computed(() => {
return this.items().reduce((sum, item) => sum + item.price, 0);
});
}

Common Pitfalls

// ❌ Mutating arrays
this.items.push(newItem);
this.items.sort();
this.items.splice(0, 1);

// ✅ Create new references
this.items = [...this.items, newItem];
this.items = [...this.items].sort();
this.items = this.items.slice(1);

// ❌ Mutating objects
this.user.name = 'Jane';

// ✅ Create new object
this.user = { ...this.user, name: 'Jane' };

// ❌ Forgetting markForCheck
setTimeout(() => {
this.count++;
}, 1000);

// ✅ With markForCheck
setTimeout(() => {
this.count++;
this.cdr.markForCheck();
}, 1000);

Best Practices

  • Always use OnPush for better performance
  • Use signals for reactive state (automatic CD)
  • Use async pipe for observables
  • Never mutate objects/arrays (use immutable updates)
  • Use trackBy with *ngFor
  • Use markForCheck() for external events
  • Prefer computed over getter functions
  • Test components with OnPush to catch issues early

Testing OnPush

describe('OnPush Component', () => {
it('should update on input change', () => {
const fixture = TestBed.createComponent(UserComponent);
const component = fixture.componentInstance;

// Set initial input
component.user = { name: 'John' };
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toContain('John');

// ✅ New reference triggers CD
component.user = { name: 'Jane' };
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toContain('Jane');
});
});