Maîtriser la détection de changements Angular avec ChangeDetectionStrategy.OnPush, markForCheck() et detectChanges() pour optimiser les performances.
Qu'est-ce que ChangeDetectionStrategy ?
ChangeDetectionStrategy contrôle la fréquence et le moment où Angular vérifie si un composant a changé et doit être re-rendu.
Deux stratégies disponibles:
- Default — Angular vérifie TOUS les composants à chaque événement.
- OnPush — Angular vérifie seulement si les @Input changent ou un événement est déclenché.
Default vs OnPush
Strategy Default (comportement par défaut):
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<p>Count: {{ count }}</p>`
// changeDetection: ChangeDetectionStrategy.Default (par défaut)
})
export class CounterComponent {
count = 0;
}
À chaque événement (click, input, timer), Angular vérifie ce composant <strong>et tous ses enfants</strong>. Inefficace sur les listes énormes!
Strategy OnPush (optimisé):
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<h3>{{ user.name }}</h3>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
@Input() user: { name: string } = {};
}
Angular vérifie ce composant SEULEMENT si:
- Un @Input change (par référence).
- Un événement Output est déclenché.
- Tu l'as marqué manuellement avec
markForCheck()oudetectChanges().
Piège courant
Avec OnPush, modifier une propriété d'objet IN-PLACE ne trigger PAS la détection !
// ❌ Doesn't work with OnPush
this.user.name = 'Alice';
// ✅ Works: nouvelle référence
this.user = { ...this.user, name: 'Alice' };
markForCheck() : marquer pour vérification
Informe Angular que ce composant doit être vérifié au prochain cycle de détection (sans re-render immédiat).
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-timer',
template: `<p>{{ elapsed }}s</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent {
elapsed = 0;
constructor(private cdr: ChangeDetectorRef) {
setInterval(() => {
this.elapsed++;
// Marquer le composant comme "à vérifier"
// sera re-rendu au prochain cycle Angular
this.cdr.markForCheck();
}, 1000);
}
}
detectChanges() : forcer la détection immédiate
Vérifie immédiatement ce composant et ses enfants, sans attendre le prochain cycle Angular.
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-live-search',
template: `
<input (keyup)="onSearch($event)">
<div>Results: {{ results.length }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LiveSearchComponent {
results: any[] = [];
constructor(private cdr: ChangeDetectorRef) {}
onSearch(event: Event) {
const query = (event.target as HTMLInputElement).value;
// Appel synchrone direct
this.results = this.search(query);
// Force l'affichage immédiatement
this.cdr.detectChanges();
}
search(query: string): any[] {
// Simulation recherche
return [];
}
}
markForCheck()→ re-render au prochain cycle (async).detectChanges()→ re-render immédiatement (sync).
Cas d'usage : composants immuables
OnPush shine quand tu utilises des patterns immuables:
// Parent
@Component({
selector: 'app-todo-list',
template: `
<app-todo-item
*ngFor="let item of todos"
[todo]="item"
(toggleDone)="toggleTodo($event)"
></app-todo-item>
`
})
export class TodoListComponent {
todos = [
{ id: 1, title: 'Learn Angular', done: false }
];
toggleTodo(id: number) {
// Créer un nouvel tableau (immuable)
this.todos = this.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
}
// Enfant (avec OnPush)
@Component({
selector: 'app-todo-item',
template: `<input type="checkbox" [checked]="todo.done">`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoItemComponent {
@Input() todo!: any;
@Output() toggleDone = new EventEmitter<number>();
}
Patterns d'optimisation avec OnPush
Pattern 1: Observable streams avec async pipe
@Component({
template: `{{ data$ | async | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataComponent {
data$ = this.http.get('/api/data');
constructor(private http: HttpClient) {}
}
Pattern 2: Signals (Angular 16+)
@Component({
template: `{{ user() | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
user = signal({ name: 'Alice' });
}
Pattern 3: Immutable libraries
// Avec Immer.js
produce(this.todos, draft => {
draft[0].done = !draft[0].done;
});
Bonnes pratiques production
- Default pour les petits projets — coût de la détection est négligeable.
- OnPush pour les listes/grilles énormes — effet significatif sur perf.
- Toujours utiliser immuabilité avec OnPush (sinon ça ne marche pas).
markForCheck()pour les updates async (timers, WebSockets).detectChanges()pour les mises à jour synchrones urgentes.- Profiler avec DevTools Angular avant d'optimiser.
- Combiner OnPush +
trackBydans *ngFor = super performant.