← Retour au blog
INPAngular performanceWeb VitalsChange DetectionSignals

INP : rendre votre app Angular réactive aux interactions utilisateur sans sacrifier la performance

L'INP (Interaction to Next Paint) est devenu la métrique centrale de la Core Web Vitals depuis mars 2024. Contrairement à la FCP ou au LCP qui mesurent des événements passifs, l'INP capture la totalité du cycle événement → traitement JavaScript → rendu. Une app Angular réactive dépend donc de trois facteurs : l'isolation des événements, la minimisation du travail CPU entre l'input et le paint, et la gestion des tâches longues. Les seuils sont stricts : 200 ms pour « bon », 500 ms pour « à améliorer ». Au-delà, l'utilisateur ressent du lag palpable. Sur mobile, où les processeurs sont limités, cette latence est amplifiée. Les équipes qui ignorent l'INP constatent souvent des rebonds de session et une conversion dégradée, même avec des LCP impeccables.

Comprendre le cycle d'interaction en Angular

Quand un utilisateur clique, tape ou scroll, le navigateur crée un événement. Angular doit capturer cet événement, le traiter dans la zone NgZone, éventuellement faire appel à des services, puis déclencher la détection de changement et enfin peindre le DOM. Chaque étape ajoute de la latence. Si votre handler d'événement lance une requête HTTP synchrone (techniquement avec forkJoin bloquant), ou si la détection de changement traverse 500 composants, vous dépasserez facilement 200 ms. Le problème s'aggrave si vous mélangez animations DOM, recalcul de styles et layout thrashing. Pour diagnostiquer, ouvrez les Web Performance APIs du navigateur : PerformanceObserver avec 'event' vous montrera exactement où traîne votre interaction. Vous verrez l'événement, la durée de traitement Angular (longTasks), et le temps avant le paint.

Considérez ce pattern courant qui ralentit : une barre de recherche qui filtre une liste de 10 000 éléments synchronement. À chaque frappe, Angular parcourt tous les éléments, recalcule les classes CSS, et le navigateur reflux le DOM. Même avec OnPush , si vous naviguez le DOM en JavaScript avant le paint, vous forcez un layout thrashing. La solution : déléguez le filtrage à un Web Worker, ou utilisez un Subject avec debounceTime(200) et distinctUntilChanged() pour regrouper les frappes. Ainsi, vous ne traitez qu'une requête par 200 ms, et le paint n'est déclenché qu'une fois. L'autre approche consiste à virtualiser la liste avec @angular/cdk et un ChangeDetectionStrategy.OnPush strict : seuls les 50 éléments visibles sont rendus.

Optimiser la détection de changement

La détection de changement Angular est le coupable silencieux de l'INP lent. Par défaut, Default traverse tous les composants. Pour une page avec 200 composants, cela veut dire 200 checks, même si seuls 5 ont changé. Basculez tous les composants en OnPush : cela rend Angular réactif uniquement aux @Input modifiés, aux événements, ou aux observables émettant. Le gain peut être 5 à 10x sur des apps complexes. Utilisez inject(ChangeDetectorRef) pour manuellement déterminer quand redessiner. Ensuite, exploitez les Signaux Angular (v16+) : quand un Signal change, seuls les consommateurs sont mis à jour, pas tout le graphe de composants. Les Signaux sont plus rapides que les Observables pour l'INP car ils ne passent pas par NgZone et la détection asynchrone.

Voici un exemple concret. Au lieu de :

export class SearchComponent {
  @Input() items: any[];
  filteredItems = this.items; // Chaque changement de parent retrigger la détection
  
  onSearch(term: string) {
    this.filteredItems = this.items.filter(i => i.name.includes(term));
  }
}

Préférez :

export class SearchComponent implements OnInit {
  items = input<any[]>([]);
  searchTerm = signal('');
  filteredItems = computed(() => 
    this.items().filter(i => i.name.includes(this.searchTerm()))
  );
  
  constructor() {
    effect(() => console.log('Filtered:', this.filteredItems()));
  }
  
  onSearch(term: string) {
    this.searchTerm.set(term);
  }
}

Avec Signals, l'update est granulaire et synchrone. Pas de markForCheck() , pas de NgZone. Aucun parent n'est notifié.

Tâches longues et chunking

Si une interaction déclenche un calcul qui prend 350 ms (parsing JSON massif, transformation d'image, algorithme complexe), vous dépassez 200 ms avant même que le paint ne survienne. Le navigateur ne peut pas interrompre ce travail, et l'utilisateur voit du jank. La solution : découpez le travail en tâches plus courtes avec scheduler.yield() (ou setTimeout(..., 0) ) pour laisser le navigateur respirer et traiter d'autres interactions. En Angular, utilisez requestIdleCallback pour les tâches non-urgentes, ou planifiez avec RxJS asyncScheduler . Pour les workers intensifs, déplacez-les dans un Web Worker : le calcul s'exécute sur un thread séparé, le thread principal reste libre pour l'interaction.

Un exemple : vous chargez 50 000 logs à analyser. Au lieu de les traiter d'un coup :

processLogs(logs: Log[]) {
  logs.forEach(log => this.analyzeLog(log)); // Bloque 400 ms
}

Chunquez :

processLogs(logs: Log[]) {
  const chunkSize = 500;
  let index = 0;
  
  const processChunk = () => {
    const chunk = logs.slice(index, index + chunkSize);
    chunk.forEach(log => this.analyzeLog(log));
    index += chunkSize;
    
    if (index < logs.length) {
      scheduler.yield().then(processChunk);
    }
  };
  
  processChunk();
}

Chaque chunk prend ~10 ms, laissant 90 ms libres pour les interactions utilisateur.

Pièges courants à éviter

Beaucoup d'équipes optimisent le LCP puis s'étonnent que l'INP reste mauvais. Le piège : avoir une page qui s'affiche vite mais devient lente à l'interaction. Cela arrive souvent avec des frameworks de charting ou des tableaux. Vous chargez un dataset petit (LCP bon), mais au clic sur un filtre, vous recalculez 100 graphiques. L'INP explose. La solution : chargez les données intelligemment, prérécalculez les agrégations côté serveur, et limitez les re-rendus avec virtualisation ou pagination. Un autre piège : oublier que les événements réseau ne doivent pas bloquer l'interaction. Si un clic déclenche une requête HTTP et que vous attendez synchronement la réponse (avec toPromise() bloquant), vous violez l'INP. Toujours gérer les requêtes de manière asynchrone, afficher un skeleton ou un loader, et mettre à jour le DOM quand la réponse arrive. Enfin, attention aux polices web chargées tardivement : un paint peut être retardé si la police n'est pas disponible. Utilisez font-display: swap pour forcer un rendu avec la police de secours immédiatement.

Mesurer et monitorer l'INP en production

L'INP n'est pas une métrique moyenne : elle capture le 98e percentile des interactions. Un utilisateur qui clique 100 fois et a une latence de 250 ms sur un clic dégrade son score. Utilisez web-vitals npm ou PerformanceObserver pour envoyer les INP observées à votre analytics. Configurez des alertes : si l'INP dépasse 200 ms pour plus de 10 % des sessions, déclenchez une notification. En développement, utilisez Lighthouse en mode throttling mobile (Slow 4G, CPU 4x) pour simuler les conditions réelles. Les DevTools modernes affichent aussi les interactions dans l'onglet Performance. Enregistrez une session utilisateur type, regardez le flame graph : si vous voyez un JavaScript rouge qui prend 150 ms, c'est du chunking manqué ou une détection de changement excessive.

Conclusion

L'INP requiert une discipline architecturale : OnPush partout, Signals pour les mises à jour rapides, chunking pour les tâches longues, et monitoring constant. Contrairement au LCP (une seule mesure au chargement), l'INP dépend de chaque interaction utilisateur. Une optimisation qui gagne 50 ms sur le LCP mais en perd 100 sur l'INP est contre-productive. Commencez par auditer votre app avec Lighthouse et Web Vitals, identifiez les composants qui font traîner les événements, puis appliquez les remèdes : OnPush, Signals, virtualisation, Web Workers. Sur Ionic/Capacitor, l'INP est encore plus critique car le mobile est limité. Les apps qui passent sous 200 ms constatent une meilleure rétention utilisateur et une conversion augmentée. Testez régulièrement, mesurez en production, et itérez.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️