All articles
/18 min read

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.

Benjamin Tietz
Benjamin TietzFreelance Angular & DevSecOps Engineer
Core Web Vitals 2026: Understanding and Optimizing INP in Angular

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.

Core Web Vitals: Real-time performance monitoring is crucial for good INP scores.
Core Web Vitals: Real-time performance monitoring is crucial for good INP scores.

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:

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

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.

bash
[Button Click]
  → Zone.js detects event
  → Angular checks all 200 components in the tree
  → Unnecessary DOM updates are computed
  → Frame is painted
  → INP: 380ms  ← bad

With OnPush and Signals, this improves. With Zoneless, the problem disappears structurally.

Modern performance monitoring: Every interaction counts.
Modern performance monitoring: Every interaction counts.

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:

bash
ng generate @angular/core:zoneless

The schematic removes zone.js from polyfills and sets up the providers:

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

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.

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. Since March 2024, INP is officially the Core Web Vital for interactivity — FID is no longer measured or evaluated by Google.
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.
Under 200 ms is "good". Values between 200–500 ms ("needs improvement") warrant systematic analysis. Above 500 ms: act immediately, it measurably costs conversions.
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.

Questions about Angular, DevSecOps, or infrastructure? I'm available freelance.

Get in touch