All articles
/18 min read

Measure and Optimize INP in Angular | Core Web Vitals

Measure and optimize Interaction to Next Paint in Angular: thresholds, DevTools, zoneless, OnPush, @defer, and web workers.

Benjamin Tietz
Benjamin TietzFreelance Angular & DevSecOps Engineer
Measure and Optimize INP in Angular | Core Web Vitals

Interaction to Next Paint (INP) replaced FID as a Core Web Vital on March 12, 2024. That technical milestone is not this article's publication date: the article was published on March 20, 2026 and technically updated on August 13, 2026. A good INP is at most 200 ms at the 75th percentile. In Angular, the practical levers are short event handlers, targeted change detection, and moving heavy work off the interaction path.

What Changed in March 2024: FID Is Gone

Since March 2024, First Input Delay (FID) is history. Interaction to Next Paint (INP) is the new Core Web Vital for interactivity — and it measures something fundamentally different.

FID only measured the delay until the first input was processed. INP measures the total latency of every interaction on the page: the time from a click or keypress to the next visible frame in the browser. That's significantly stricter.

INP includes input delay, processing time, and presentation delay up to the next visible frame.
INP includes input delay, processing time, and presentation delay up to the next visible frame.

Field data and lab measurements answer different questions: Chrome UX Report and Search Console reflect real usage over time, while DevTools and Lighthouse analyze a reproducible flow. An optimization should therefore never be derived from one isolated lab run.

What Exactly Is INP?

INP stands for Interaction to Next Paint. It measures the time from the start of a user interaction (click, touch, keypress) to the moment the browser paints the next frame.

  • Input Delay — Time until the event handler starts (blocked by other JS code on the main thread)
  • Processing Time — Time the event handler itself takes
  • Presentation Delay — Time until rendering is complete

Field assessment uses the 75th percentile of page views, segmented by mobile and desktop. Within one page view, INP represents approximately the slowest qualifying interaction; outliers are bounded when many interactions occur.

Thresholds

Under 200 ms is considered good. Values between 200–500 ms signal room for improvement. Above 500 ms is poor — immediate action is required.

Measuring INP: The Right Tools

Before optimizing, you need to measure. There are several complementary approaches.

Chrome DevTools — Performance Panel

The Performance Panel shows INP candidates directly. Since Chrome 122, INP is highlighted as a standalone metric.

  • Open DevTools → Performance tab
  • Reload page and interact with the app
  • Stop recording → Check the "Interactions" lane
  • Identify long interactions (marked orange/red)

Integrating web-vitals.js

For Real User Monitoring (RUM) in your own app:

typescript
import { onINP } from 'web-vitals';

onINP(({ value, rating, entries }) => {
  console.log(`INP: ${value}ms (${rating})`);
  // Send to your analytics endpoint
  sendToAnalytics({ metric: 'INP', value, rating });
});

Why Angular with Zone.js Is Vulnerable

Older Angular applications commonly use Zone.js for change detection. Zone.js patches common asynchronous browser APIs and tells Angular after tasks that application state may have changed.

The issue is not that Zone.js itself writes to the DOM. It can, however, schedule application synchronization more often than necessary. The cost depends on the component tree, change-detection strategy, and the work performed by the event handler.

bash
[User interaction]
  → Zone.js schedules application synchronization
  → Angular traverses the relevant views
  → changed bindings are updated
  → the browser paints the next frame

OnPush can skip subtrees; Signals notify Angular about state used by templates. Zoneless removes Zone.js as a global task trigger, but it does not replace clean state boundaries or measurement of the actual interaction.

RUM field data exposes real usage; a performance trace then explains the slow interaction.
RUM field data exposes real usage; a performance trace then explains the slow interaction.

Optimization 1: Zoneless Change Detection

Since Angular 20.2, Zoneless is marked as stable — since Angular 21, it's the default for new projects.

In Angular 21, zoneless is the default for new applications. Existing Angular 20 applications enable it with the stable provider; on Angular 21, make sure provideZoneChangeDetection() does not override the default:

typescript
// Angular 20: app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [provideZonelessChangeDetection()]
};

// Angular 21+: zoneless is the default.
// Then remove zone.js from build/test polyfills and dependencies.

Zoneless schedules change detection through explicit Angular notifications such as updated Signals, template listeners, input updates, or markForCheck(). Whether INP improves must be demonstrated for the same interaction before and after migration in field and lab data; an unsupported success number is not a credible case study.

Optimization 2: OnPush as an Immediate Fix

If a full Zoneless migration isn't possible yet: ChangeDetectionStrategy.OnPush for all components is the fastest single measure.

  • An @Input() value changes (by reference)
  • An event is fired within the component
  • markForCheck() is explicitly called
  • A bound Observable emits a new value
typescript
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductListComponent {}

Rule of thumb: Every component without OnPush is a potential INP bomb.

Less overhead, more performance: Zoneless Change Detection.
Less overhead, more performance: Zoneless Change Detection.

Optimization 3: @defer for Non-Critical Content

@defer is one of the most underrated performance features since Angular 17. It allows lazy loading parts of the template — keeping the main thread free during initial render.

html
<!-- Load comments section when visible in viewport -->
@defer (on viewport) {
  <app-comments [postId]="post.id" />
} @placeholder {
  <div class="comments-skeleton">Loading comments…</div>
}

<!-- Load heavy chart component on user interaction -->
@defer (on interaction) {
  <app-performance-chart [data]="metrics" />
} @placeholder {
  <button class="btn btn--secondary">Show chart</button>
}

Optimization 4: Heavy Computations in Web Workers

CPU-intensive algorithms directly block the main thread. The result: Input Delay increases, INP worsens.

typescript
// product-filter.worker.ts
self.onmessage = ({ data }) => {
  const { products, filters } = data;
  const filtered = products.filter(p => matchesFilters(p, filters));
  self.postMessage(filtered);
};

// component.ts
private readonly worker = new Worker(
  new URL('./product-filter.worker', import.meta.url),
  { type: 'module' }
);

filterProducts(filters: Filters) {
  this.worker.postMessage({ products: this.allProducts(), filters });
  this.worker.onmessage = ({ data }) => this.filteredProducts.set(data);
}

The filter logic now runs in parallel in the worker thread — the main thread stays free for rendering.

Optimization 5: Keep Event Handlers Lean

A common INP killer is too much logic directly in the event handler. Everything after the first frame should be deferred.

typescript
// ❌ Problematic
onClick() {
  this.processLargeDataset();     // 200ms
  this.updateMultipleSignals();   // 50ms
  this.triggerAnimations();       // 30ms
  // INP: ~280ms
}
typescript
// ✅ Better — free the main thread immediately
onClick() {
  // Immediate visual feedback
  this.isLoading.set(true);

  // Defer heavy work after the frame
  setTimeout(() => {
    this.processLargeDataset();
    this.updateMultipleSignals();
    this.isLoading.set(false);
  }, 0);
}

setTimeout(0) moves work into a new task and can allow earlier visual feedback. It is not a universal INP fix: long follow-up tasks still need to be split or moved to a web worker and measured again.

Infrastructure performance: From server to browser — every millisecond counts.
Infrastructure performance: From server to browser — every millisecond counts.

Summary: INP Checklist for Angular

  • OnPush for all components — Effort: medium, Impact: high
  • Signals instead of Observables for UI state — Effort: medium, Impact: high
  • Zoneless Migration — Effort: high, Impact: very high
  • @defer for non-critical blocks — Effort: low, Impact: medium–high
  • Web Workers for CPU-intensive work — Effort: high, Impact: situational
  • Lean event handlers — Effort: low, Impact: medium

FAQ

Yes. In March 2024, Interaction to Next Paint (INP) replaced First Input Delay (FID) as the Core Web Vital for interactivity — FID is no longer measured or evaluated by Google. FID only measured the delay before the first input was processed; INP measures the full latency of every interaction and is reported at the 75th percentile. Under 200 ms is good, 200–500 ms needs improvement, above 500 ms is poor.
Yes. INP measures client-side interactions after initial load. SSR improves LCP (Largest Contentful Paint) but doesn't help with INP — change detection still runs in the browser after hydration.
Up to 200 ms is good. Values above 200 through 500 ms need improvement, and values above 500 ms are poor. These thresholds classify user experience; any specific conversion impact requires separate evidence from your own data.
Yes. With SSR, the @placeholder content is rendered server-side. The actual component only loads client-side when the defined trigger fires.
No — but the combination is strongest. Components with OnPush + Signals benefit immediately from Zoneless. Components without OnPush require manual markForCheck() calls after migration.

Primary sources

Questions about Angular, DevSecOps, or infrastructure? I build and operate systems like these.

Discuss a project