Skip to main content
ModPageSpeed 2.0 and mod_pagespeed 1.1 — Now available

INP on ASP.NET Core: Razor vs Blazor

By Otto van der Schaaf

core-web-vitals inp aspnet-core

ASP.NET Core has two completely different INP profiles depending on the frontend. Razor Pages / MVC with vanilla JS or jQuery has a fixable INP problem — the WeAmp.PageSpeed ASP.NET Core middleware applies the same defer_javascript, combine_javascript, and rewrite_javascript filters as the nginx module, run in-process. Blazor Server has an INP problem that is architectural and unfixable without changing the rendering model: every interaction is a SignalR round-trip, which means INP is bounded below by network latency plus server render time. Both cases covered below.

What INP measures

Interaction to Next Paint (INP) is the worst-case latency between a user input — click, tap, keypress — and the next frame the browser paints in response. Chrome records every interaction over the lifetime of the page and reports the 98th-percentile slowest one as that page’s INP. Good is ≤ 200 ms, “needs improvement” is up to 500 ms, anything above 500 ms is failing. Real-user numbers come from CrUX; lab approximations come from Lighthouse via PSI, but lab INP is unreliable because the lab runs no human input. Field data is the only number that matters. See web.dev/inp.

The most common INP failures on ASP.NET Core

The failures split cleanly along the rendering-model line. Pick the section that matches your stack.

Razor Pages / MVC with vanilla JS or jQuery:

  1. bundle.min.js loaded synchronously in <head>. Visual Studio’s default templates still put <script src="~/lib/jquery/dist/jquery.min.js"></script> (and bootstrap.bundle.min.js, and jquery-validation) in <head> ahead of the LCP element. Even with defer, the parse + init still has to complete before the first interaction can be handled — typically 100–250 ms of cost on a mid-range Android.
  2. Anti-forgery token validation on POST inflates handler latency. Razor Pages auto-validate the anti-forgery token on every POST handler. If the handler also does a DB call, the synchronous validate-then-handle path means form-submit INP includes network + token-validate + handler. 300 ms+ is common.
  3. jQuery validation runs synchronously on every keystroke. jquery.validate.unobtrusive.js attaches keyup and blur validators that re-run on every input event. On a form with 8+ fields, the per-keystroke handler can hit 30–60 ms, recorded as INP on the worst keystroke.

Blazor Server:

  1. Every interaction is a SignalR round-trip. Blazor Server renders on the server and pushes DOM diffs over SignalR. INP on a button click = network latency + server render time + diff apply. For a transatlantic user (~150 ms RTT), the floor before any work is done is already at the “Needs improvement” boundary.
  2. Disconnected-then-reconnect spikes INP. SignalR reconnect on a flaky network causes the next interaction’s INP to balloon to seconds. Reconnect handling is by design but is INP-visible.
  3. Component re-render on parent state change cascades to children. A StateHasChanged() call near the top of the component tree triggers re-render of everything below; the diff sent over SignalR can be megabytes. INP on the click that triggered it is the wait for the full diff to apply.

Identify the worst interaction

Identifying which interaction records the bad INP is more important on ASP.NET Core than elsewhere because the answer dictates whether MPS is even the right tool.

  • Open the page in Chrome, DevTools → Performance panel, record, perform a realistic user flow (nav click, form fill, submit). Stop. The Interactions track names the worst interaction; clicking it shows the flame chart.
  • Install web-vitals with attribution in _Layout.cshtml. Log onINP(console.log, {reportAllChanges: true}) from a small <script type="module"> block. The attribution.eventEntry field names the responsible handler.
  • Run PSI: https://pagespeed.web.dev/analysis?url=<URL>. Lab INP numbers are unreliable; the field-data section under “Discover what your real users are experiencing” is the one to read.
  • For Blazor Server specifically: open DevTools → Network → filter for “WebSocket”. Each interaction shows as a frame on the SignalR connection. The time from frame-send to frame-receive is the network floor on your INP.

Output: a specific interaction, the script (or _blazor SignalR frame), and whether the cost is parse, handler logic, or network round-trip.

Step 2: Reduce parse + transfer cost (Razor / MVC only; Blazor Server is out of scope)

This step splits by rendering model. Pay attention to which paragraph applies.

For Razor Pages / MVC sites, the WeAmp.PageSpeed ASP.NET Core middleware (NuGet) runs the same optimization pipeline as the nginx module, in-process. Enable, in order:

  • defer_javascript — pushes script execution past the first paint so jQuery + jQuery Validation parse no longer collides with the first click. Marked Test first in the filter reference because it changes execution order — code in <script> blocks that depends on $(document).ready running before other scripts may break. Stage it.
  • combine_javascript — concatenates the bundle.min.js + jQuery + jQuery Validation + Bootstrap chain into one file. Cuts parser-setup overhead.
  • rewrite_javascript — minifies inline and external JS. The .min.js files are already minified, but inline <script> blocks in Razor partials usually are not.

Wire it up in Program.cs:

using WeAmp.PageSpeed.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPageSpeed(options =>
{
    options.Cache.VolumePath = "/data/cache.vol";
    options.Cache.VolumeSizeBytes = 256 * 1024 * 1024;
    options.Filters.Enable("defer_javascript");
    options.Filters.Enable("combine_javascript");
    options.Filters.Enable("rewrite_javascript");
});

var app = builder.Build();
app.UsePageSpeed();

For Blazor Server, ModPageSpeed cannot help. SignalR round-trips are the dominant INP cost; no rewriter operating on the response stream can shorten a network round-trip. Skip this step entirely and go straight to the framework-level tuning below.

Where the framework still helps

For Razor Pages / MVC:

  • Replace jQuery validation with native HTML5 form validation (required, type="email", pattern=) + a 1 KB error-display helper. Drops a 30+ KB JS dependency and removes per-keystroke validator handlers.
  • Bundle and minify with WebOptimizer (Microsoft.AspNetCore.WebOptimizer.Core package). It outputs hashed, gzipped assets at build time.
  • Use <script type="module"> for ESM-style code paths. Modern browsers parse modules in a separate scheduler with lower INP impact than classic-script parse.
  • Cache anti-forgery tokens client-side per session — reuse across forms instead of re-validating each.
  • Move scripts to end-of-body via @RenderSection("Scripts", required: false) in _Layout.cshtml, not <head>.

For Blazor Server:

  • Switch to Blazor WebAssembly, or use @rendermode InteractiveAuto (.NET 8+) to fall back to WASM after the second visit. WASM runs in-browser, so INP is bounded by client-side compute instead of network round-trip.
  • If you must stay on Blazor Server: keep the user geographically close to the server. A US-region server with EU users has a 100+ ms RTT floor that no application change can remove.
  • Avoid StateHasChanged() near the component-tree root. Push state down so the re-render fan-out is small.
  • Use ShouldRender() overrides to skip diff computation on components whose state has not changed.
  • Pre-render to static HTML for non-interactive routes (@rendermode StaticSSR) — INP is irrelevant there because there is no interaction.

How to confirm it took

  1. Compare web-vitals onINP attribution logs (from the Diagnose step) before and after deploy. The worst handler should be different — and shorter — than the baseline; for Razor/MVC sites the slowest entry should now sit in the “Good” band.
  2. DevTools → Performance, re-record the user flow, confirm the Interactions track shows under-200 ms for the worst interaction.
  3. For Blazor Server: re-check the SignalR frame round-trip time in DevTools → Network → WebSocket. That number is your floor; the application can be optimized down to it but not below.
  4. Re-run PSI on the same URL. Field INP under “Interaction to Next Paint” should drop into the “Good” band for Razor/MVC sites after the rewriter + platform tuning. Blazor Server sites usually cannot exit “Needs improvement” without a re-architecture.
  5. Wait 28 days, check Search Console → Core Web Vitals report. ASP.NET Core sites typically have a single URL group; track it as a whole.

The middleware config

// Program.cs — minimal config to address INP on ASP.NET Core (Razor/MVC)
using WeAmp.PageSpeed.AspNetCore;

builder.Services.AddPageSpeed(options =>
{
    options.Cache.VolumePath = "/data/cache.vol";
    options.Cache.VolumeSizeBytes = 256 * 1024 * 1024;
    options.Filters.Enable("defer_javascript");
    options.Filters.Enable("combine_javascript");
    options.Filters.Enable("rewrite_javascript");
});

// In the pipeline, before MVC:
app.UsePageSpeed();

For sites still fronted by nginx (e.g., Kestrel behind nginx reverse proxy), the nginx config from the WordPress post works unchanged.

For Blazor Server: there is no MPS config that fixes INP. See “Where this approach falls short” below.

Where this approach falls short

The Blazor Server section, expanded because it dominates the failure modes here.

  • Blazor Server INP is architectural, not an optimization problem. SignalR round-trips are by design; the framework was built for interactive line-of-business apps on a LAN, not public sites served to users on mobile. If you are seeing INP > 500 ms on a public Blazor Server site, ModPageSpeed and every other server-layer tool will fail to move the number meaningfully. The fix is Blazor WebAssembly, @rendermode InteractiveAuto, or a different frontend. We will not pretend otherwise.
  • Razor Pages with third-party JS controls. Telerik, Syncfusion, and DevExpress UI controls ship their own JS frameworks. INP on a Telerik grid sort or DevExpress combo open is bounded by their library’s render path — MPS minifies their bytes but cannot change their internal architecture.
  • TTFB-driven INP on uncached XHR endpoints. If your interactive widgets fire fetch('/api/something') and your API endpoint takes 300 ms to respond, INP on that interaction is at least 300 ms. The fix is server-side — caching, async/await discipline in handlers, or DB indexing. Not page optimization.
  • SignalR reconnect spikes. Even on Blazor WebAssembly, if you use SignalR for real-time updates and the connection drops, the next interaction may record a multi-second INP. Implement client-side queuing.
  • StateHasChanged() on hot paths. A Blazor component that re-renders on every prop change cascades through its children. Profile with Blazor’s built-in diagnostic counters before assuming MPS is the answer.

ASP.NET Core’s INP story is almost entirely about what frontend you ship. ModPageSpeed handles the Razor / jQuery case cleanly; the Blazor Server case is one we route to the framework, not to ourselves.

The IIS-flavored angle on the same metrics, including IIS-specific deployment notes, is at iispeed.com — IIS Core Web Vitals 2026.

ModPageSpeed runs as an ASP.NET Core middleware (NuGet) or an nginx / Apache / IIS module. On ASP.NET Core, run your app and start the 14-day trial from the console at /console/ (card-at-start via FastSpring). See license terms.

Read next