OnPush Strategy
What is OnPush?
Section titled “What is OnPush?”Change detection only runs when:
- Input reference changes
- Events in template
- Async pipe emits
- 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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Immutability Helpers”// Array operationsconst newArray = [...oldArray, newItem]; // Addconst newArray = oldArray.filter(item => item.id !== id); // Removeconst newArray = oldArray.map(item => item.id === id ? { ...item, name: 'New' } : item); // Update
// Object operationsconst newObj = { ...oldObj, name: 'New' }; // Updateconst newObj = { ...oldObj, nested: { ...oldObj.nested, value: 'New' } }; // Deep update
// Using librariesimport { produce } from 'immer';const newState = produce(state, draft => { draft.user.name = 'Jane'; // Mutate draft, get immutable result});trackBy with OnPush
Section titled “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
Section titled “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
Section titled “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
Section titled “Common Pitfalls”// ❌ Mutating arraysthis.items.push(newItem);this.items.sort();this.items.splice(0, 1);
// ✅ Create new referencesthis.items = [...this.items, newItem];this.items = [...this.items].sort();this.items = this.items.slice(1);
// ❌ Mutating objectsthis.user.name = 'Jane';
// ✅ Create new objectthis.user = { ...this.user, name: 'Jane' };
// ❌ Forgetting markForChecksetTimeout(() => { this.count++;}, 1000);
// ✅ With markForChecksetTimeout(() => { this.count++; this.cdr.markForCheck();}, 1000);Best Practices
Section titled “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
trackBywith*ngFor - Use
markForCheck()for external events - Prefer computed over getter functions
- Test components with OnPush to catch issues early
Testing OnPush
Section titled “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'); });});
