← Retour au blog
INPAngular performanceWeb VitalsChange DetectionSignals

INP: Making Your Angular App Truly Responsive to User Interactions Without Sacrificing Performance

Interaction to Next Paint (INP) became the focal metric of Core Web Vitals in March 2024. Unlike FCP or LCP which measure passive events, INP captures the entire cycle: event → JavaScript processing → render. A responsive Angular app depends on three factors: event isolation, minimizing CPU work between input and paint, and long-task management. The thresholds are strict: 200 ms is "good," 500 ms is "needs improvement." Beyond that, users feel palpable lag. On mobile, where processors are constrained, this latency amplifies dramatically. Teams that ignore INP often see session bounce rates spike and conversion drop, even with flawless LCP scores.

Understanding the Interaction Cycle in Angular

When a user clicks, types, or scrolls, the browser fires an event. Angular must capture it, process it within NgZone, possibly call services, trigger change detection, and finally paint the DOM. Each step adds latency. If your event handler launches a blocking HTTP request (technically a synchronous forkJoin ), or if change detection traverses 500 components, you'll easily exceed 200 ms. The problem worsens if you mix DOM animations, style recalculation, and layout thrashing. To diagnose, open the browser's Web Performance APIs: PerformanceObserver with 'event' shows exactly where your interaction stalls. You'll see the event, Angular's processing duration (longTasks), and time until paint. The modern Performance API also exposes interactionId linking all related paints and layouts to a single user action, making root-cause analysis precise.

Consider a common pattern that kills INP: a search bar filtering a list of 10,000 items synchronously. On each keystroke, Angular traverses all items, recalculates CSS classes, and the browser reflows the DOM. Even with OnPush , if you navigate the DOM in JavaScript before paint, you force layout thrashing. The fix: delegate filtering to a Web Worker, or wrap it in a Subject with debounceTime(200) and distinctUntilChanged() to batch keystrokes. Now you process one request per 200 ms, and paint triggers once. Another approach: virtualize the list with @angular/cdk and strict ChangeDetectionStrategy.OnPush : only the 50 visible items render. The result is a search that feels instant, with INP consistently under 100 ms.

Optimizing Change Detection

Angular's change detection is the silent culprit behind sluggish INP. By default, Default strategy traverses every component. On a page with 200 components, that means 200 checks even if only 5 changed. Switch all components to OnPush : Angular now reacts only to @Input changes, events, or observables emitting. The gain can be 5–10x on complex apps. Use inject(ChangeDetectorRef) to manually control redraw timing. Next, leverage Angular Signals (v16+): when a Signal changes, only consumers update, not the entire component graph. Signals outperform Observables for INP because they bypass NgZone and async detection entirely. They're synchronous, granular, and don't trigger parent component checks.

Here's a concrete example. Instead of:

export class SearchComponent {
  @Input() items: any[];
  filteredItems = this.items; // Every parent change retriggers detection
  
  onSearch(term: string) {
    this.filteredItems = this.items.filter(i => i.name.includes(term));
  }
}

Prefer:

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);
  }
}

With Signals, updates are granular and synchronous. No markForCheck() , no NgZone overhead. No parent gets notified. The filtering happens in microseconds, paint follows immediately.

Long Tasks and Task Chunking

If an interaction triggers a calculation taking 350 ms—parsing massive JSON, image transformation, complex algorithm—you exceed 200 ms before paint even fires. The browser can't interrupt this work, and users see jank. Solution: break work into smaller chunks with scheduler.yield() (or setTimeout(..., 0) ) to let the browser breathe and process other interactions. In Angular, use requestIdleCallback for non-urgent tasks, or schedule with RxJS asyncScheduler . For CPU-intensive work, move it to a Web Worker: computation runs on a separate thread, the main thread stays free for interactions.

Example: you load 50,000 logs to analyze. Instead of processing them all at once:

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

Chunk it:

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();
}

Each chunk takes ~10 ms, leaving 90 ms free for user interactions. The UI remains responsive even under heavy computational load.

Common Pitfalls to Avoid

Many teams optimize LCP then wonder why INP remains poor. The trap: a page that displays fast but becomes sluggish on interaction. This often happens with charting or table frameworks. You load a small dataset (LCP good), but clicking a filter recalculates 100 charts. INP explodes. Fix: load data intelligently, pre-aggregate on the server, and limit re-renders with virtualization or pagination. Another pitfall: forgetting that network requests shouldn't block interaction. If a click triggers an HTTP request and you wait synchronously for the response (with blocking toPromise() ), you violate INP. Always handle requests asynchronously, show a skeleton or loader, and update the DOM when the response arrives. Finally, watch for late-loading web fonts: a paint can be delayed if the font isn't available. Use font-display: swap to force rendering with the fallback font immediately, then swap when the custom font loads.

Measuring and Monitoring INP in Production

INP is not an average metric: it captures the 98th percentile of interactions. A user clicking 100 times with one 250 ms latency spike degrades their score. Use web-vitals npm or PerformanceObserver to send observed INP to your analytics backend. Set up alerts: if INP exceeds 200 ms for over 10% of sessions, trigger notification. In development, run Lighthouse with mobile throttling (Slow 4G, CPU 4x) to simulate real conditions. Modern DevTools also show interactions in the Performance tab. Record a typical user session, examine the flame graph: if you see red JavaScript taking 150 ms, that's missed chunking or excessive change detection. Combine field data (real users) with lab data (your tests) for a complete picture. Over time, you'll identify patterns: certain actions always spike INP, certain component types are repeat offenders.

Conclusion

INP demands architectural discipline: OnPush everywhere, Signals for fast updates, chunking for long tasks, and constant monitoring. Unlike LCP (a single measurement at load), INP depends on every user interaction. An optimization gaining 50 ms on LCP but losing 100 ms on INP is counterproductive. Start by auditing your app with Lighthouse and Web Vitals, identify components dragging event latency, then apply remedies: OnPush, Signals, virtualization, Web Workers. On Ionic/Capacitor, INP is even more critical because mobile is resource-constrained. Apps staying under 200 ms consistently see better user retention and higher conversion. Test regularly, measure in production, iterate relentlessly. The difference between a 150 ms INP and a 300 ms INP is felt instantly by users—and your metrics will show it.

Développeur Angular & Mobile freelance — Strasbourg.

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