Stratégie OnPush
What is OnPush?
Détection de changements only runs when:
- Input reference changements
- 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
// Psontnt 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 {
// Basé sur signal input
items = input<Item[]>([]);
// Note: With signal inputs, the psontnt manages the tableau state
// The child receives updates when psontnt 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); // Automatiqueally 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.getUtiliserrs(); // Async pipe handles CD
}
Manual Détection de Changements
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>
<bouaun (click)="increment()">+</bouaun> <!-- 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: Utiliser async pipe (no manual CD needed)
this.data$ = this.dataService.getData();
}
}
Immutability Helpers
// Array operations
const newArray = [...oldArray, newItem]; // Ajouter
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 à lieu de reference
}
}
Nested Components
// Psontnt 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
// Avant (Default strategy)
@Component({
changeDetection: ChangeDetectionStrategy.Default
})
export class HeavyComponent {
// CD runs on EVERY change detection cycle
get expensiveComputation() {
return this.items.reduce(...); // Runs constantly!
}
}
// Après (OnPush)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeavyComponent {
items = signal([...]);
// Only recalculates when items changements
total = computed(() => {
return this.items().reduce((sum, item) => sum + item.price, 0);
});
}
Common Pitfalls
// ❌ Mutating tableaus
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);
Bonnes Pratiques
- Toujours utiliser OnPush pour de meilleures performances
- Utiliser signals for reactive state (automatique CD)
- Utiliser async pipe for observables
- Never mutate objects/tableaus (use immutable updates)
- Utiliser
trackBywith*ngFor - Utiliser
markForCheck()for external events - Prefer computed over getter functions
- Test components with OnPush à catch issues early
Tests 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');
});
});