Core Web Vitals 2026: Understanding and Optimizing INP in Angular
Interaction to Next Paint (INP) replaced FID. How Angular apps achieve sub-200ms with zoneless, signals, and @defer.
Interaction to Next Paint (INP) officially replaced FID as a Core Web Vital in March 2024. A good INP score is under 200 ms. Angular apps using Zone.js are structurally vulnerable to poor INP scores — the solution lies in Zoneless Change Detection, @defer blocks, and deliberate event handler optimization.
What Changed in 2025/2026
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.
As of March 2026, 57.1% of desktop websites pass the Core Web Vitals check — but only 49.7% of mobile pages. The INP gap is the main reason. Single-page applications like Angular apps have a structural deficit here.
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
Google uses the 75th percentile value of all interactions — not the average. A single slow interaction can degrade the entire INP score.
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
Angular traditionally uses Zone.js for change detection. Zone.js patches all asynchronous browser APIs — setTimeout, Promise, fetch, event listeners — and notifies Angular after every operation: "Something changed, please re-render."
The problem: Zone.js re-renders too much. Every event, every click, every timer triggers change detection across the entire component tree — even when nothing relevant has changed.
[Button Click]
→ Zone.js detects event
→ Angular checks all 200 components in the tree
→ Unnecessary DOM updates are computed
→ Frame is painted
→ INP: 380ms ← badWith OnPush and Signals, this improves. With Zoneless, the problem disappears structurally.
Optimization 1: Zoneless Change Detection
Since Angular 20.2, Zoneless is marked as stable — since Angular 21, it's the default for new projects.
For existing projects, the Angular CLI provides a migration schematic:
ng generate @angular/core:zonelessThe schematic removes zone.js from polyfills and sets up the providers:
// app.config.ts — after migration
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
// other providers...
]
};With Zoneless, Angular only re-renders when a signal changes its value — precise, minimal, fast. In my own setup, I reduced INP values from an average of 340 ms to under 120 ms after the Zoneless migration.
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);
}With setTimeout(0), Angular gives the browser a chance to render visual feedback first. The user sees an immediate response — INP drops because the first frame is painted quickly.
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.