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.

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.

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:
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.
[User interaction]
→ Zone.js schedules application synchronization
→ Angular traverses the relevant views
→ changed bindings are updated
→ the browser paints the next frameOnPush 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.

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:
// 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
@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.

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.
<!-- 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.
// 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.
// ❌ Problematic
onClick() {
this.processLargeDataset(); // 200ms
this.updateMultipleSignals(); // 50ms
this.triggerAnimations(); // 30ms
// INP: ~280ms
}// ✅ 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.

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
@placeholder content is rendered server-side. The actual component only loads client-side when the defined trigger fires.OnPush + Signals benefit immediately from Zoneless. Components without OnPush require manual markForCheck() calls after migration.