ChangeDetectionStrategy en Angular : DetectChanges et OnPush

🏷️ Front-end 📅 12/04/2026 02:00:00 👤 Mezgani Said
Angular Change Detection Performance Detectchanges
ChangeDetectionStrategy en Angular : DetectChanges et OnPush

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.

Point clé: bien gérer la détection de changement = performances massives sur les gros projets.

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() ou detectChanges().

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);
  }
}
Quand l'utiliser: quand tu modifies l'état depuis une source externe (timer, socket, async). Le changement sera visible au prochain tick Angular.

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>();
}
Résultat: avec 1000 items, seul celui modifié est re-rendu (grâce à la nouvelle référence du tableau). Default aurait checké les 1000!

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 + trackBy dans *ngFor = super performant.
A retenir: ChangeDetectionStrategy.OnPush + Immuabilité = performances Angular surhumaines.