ASP.NET Core on IIS: the WeAmp.PageSpeed.AspNetCore shape
Updated 2026-10-22 · We-Amp B.V. · about an 8-minute read
The most common .NET enterprise topology we see is ASP.NET Core deployed in-process behind IIS, fronted by the ASP.NET Core Module (ANCM). Customers ask us a recurring question: where does image and CSS optimization fit when the request flows through IIS, then ANCM, then Kestrel, then back out? Architectural map first, then the practical Program.cs.
The request path, drawn out
ANCM is a native IIS module that forwards incoming requests into an
in-process Kestrel instance hosted inside the IIS worker process
(w3wp.exe). The path looks like:
browser
-> IIS HTTP.SYS
-> w3wp.exe (IIS worker)
-> ASP.NET Core Module (in-process)
-> Kestrel
-> ASP.NET Core middleware pipeline
-> your application
Two things follow from that path. First, the IIS-level handlers
(static-file handler, dynamic compression, URL rewrite) only see
requests that hit IIS-managed paths, not requests that ANCM forwards
to Kestrel. Second, ASP.NET Core's
UseStaticFiles() middleware serves the static assets,
which means any request-time optimization has to happen inside the
ASP.NET Core pipeline, not at the IIS layer.
That is why the IISpeed-style native IIS module is the wrong layer for an ASP.NET Core application. The mod_pagespeed IIS module plugs into the IIS request pipeline at a level that ANCM has already bypassed for ASP.NET Core requests. The module is the right tool for classic ASP.NET, WebForms, and SharePoint, but the wrong tool for an application whose every request gets handed off to Kestrel.
Where WeAmp.PageSpeed.AspNetCore fits
WeAmp.PageSpeed.AspNetCore is the ASP.NET Core form of
mod_pagespeed — the same PageSpeed Automatic optimization
library as the IIS module, repackaged for the
IApplicationBuilder idiom and shipped as a NuGet
package. The middleware sits inside the Kestrel pipeline, so it sees
every request whether you host behind IIS via ANCM, run Kestrel
directly behind a reverse proxy, or run a container on a different
OS entirely.
Registration in Program.cs
Add the package, then register it:
dotnet add package WeAmp.PageSpeed.AspNetCore
using WeAmp.PageSpeed.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Registers the services and binds the "PageSpeed" section of
// appsettings.json. Enabled, Html and the other top-level keys
// hot-reload; the Cache and Worker sections are read once at startup.
builder.Services.AddPageSpeed();
builder.Services.AddRazorPages();
var app = builder.Build();
// Order matters: UsePageSpeed before anything that writes the response
// body — before UseStaticFiles so it sees the static-asset responses,
// before the endpoints so it can rewrite the application's HTML.
app.UsePageSpeed();
app.UseStaticFiles();
app.MapRazorPages();
app.Run();
The middleware reads each response, decides whether to optimize
(HTML, CSS, JavaScript, images), and rewrites the body in place.
Bytes that should pass through (application/json API
responses, and anything under a prefix listed in
ExcludePaths — /api/,
/signalr/, /_blazor/ and
/_framework/ by default) skip the optimization layer.
UsePageSpeed() also mounts the middleware's
/console/ admin SPA and its worker-API proxy on the
public site, on by default and with no credential of its own. Treat
it as an admin surface: gate the path at the IIS layer (and set
Console.RequireHttps so it is never served in the
clear), move it off a guessable path with
Console.MountPath, or switch it off with
Console.Enabled: false — the package README's Security
section has the detail.
The options live in the PageSpeed section of
appsettings.json (or any other
IConfiguration source), which
AddPageSpeed() binds. Every key is optional and falls
back to its default — the five Html toggles below are
spelled out at their defaults, so you can see what there is to
flip; the
configuration
reference lists them all.
{
"PageSpeed": {
"Enabled": true,
"Cache": {
"VolumePath": "C:\\ProgramData\\WeAmp.PageSpeed\\cache\\volume.dat"
},
"Html": {
"EnableCriticalCss": true,
"EnableLazyLoad": true,
"EnableImageDimensions": true,
"EnableLcpPreload": true,
"EnableScriptDeferral": true
}
}
}
Then dotnet run and curl -i a content
route: the response carries X-PageSpeed: MISS on the
first request and HIT once the worker has built the
variant — the
verify
steps on modpagespeed.com cover the rest.
The switches worth knowing about
There is no rewrite level and no filter list: the image pipeline is
on out of the box, and each HTML rewrite is a boolean under
PageSpeed:Html. The ones that do most of the Core Web
Vitals work:
-
Image transcoding — not a toggle. The middleware
serves WebP or AVIF at the original URL to browsers that advertise
the format in the request's
Acceptheader, with viewport-aware resizing. Hero-image weight typically drops by an order of magnitude on image-heavy Razor pages. -
EnableImageDimensionsandEnableLazyLoad: explicitwidthandheighton images that omit them (Cumulative Layout Shift) andloading="lazy"on below-the-fold images;EnableLcpPreloadadds the preload hint for the detected LCP image. -
EnableCriticalCss: extracts the above-the-fold CSS rules and inlines them in the document head. SetEnableAsyncCss: trueas well (off by default) to defer the remaining stylesheets through the middleware's async loader — that pair is what takes the stylesheet round trip off first paint. -
EnableScriptDeferral: defers scripts based on browser coverage analysis, moving non-critical JavaScript out of the critical path. Interaction to Next Paint improves on heavy Razor pages with multiple jQuery plugins or vendored widgets.
Interaction with IIS-level features
Hosting ASP.NET Core behind IIS means a handful of IIS-level features are still in the path even though they do not see the application bytes:
-
IIS Static Content Compression. Leave it on. The
middleware emits CSS and JavaScript that compresses well, and the
IIS compression layer applies gzip / Brotli to the response before
it leaves
w3wp.exe. The two features stack rather than overlap. -
HTTP/2. IIS terminates HTTP/2 at HTTP.SYS and
downgrades the in-process forward to HTTP/1.1, so Kestrel receives
an HTTP/1.1 request from ANCM regardless of what the client spoke.
The middleware's behaviour is the same on HTTP/1.1 and HTTP/2; no
change in configuration. The LCP-preload and preconnect hints go
out as
Link: rel=preload/rel=preconnectresponse headers, which IIS forwards untouched; nothing is pushed. -
URL Rewrite Module. Any URL rewrite rules in
web.configexecute at the IIS layer before ANCM hands off to Kestrel; they affect what URL the application sees but do not interact with the optimization layer. -
Application-pool identity and the cache volume.
The middleware keeps optimized variants in a single cache volume
file,
PageSpeed:Cache:VolumePath. Its default,/var/cache/pagespeed/volume.dat, is a Linux path, so on Windows set it explicitly — theappsettings.jsonabove usesC:\ProgramData\WeAmp.PageSpeed\cache\volume.dat. Make it absolute: under in-process hosting a relative path resolves againstw3wp.exe's working directory, not the application's. The middleware creates the folder on first start if it is missing, but it does so as the application-pool identity (commonlyIIS APPPOOL\<site>), so grant that identity Modify on the parent folder beforehand — nothing in the package sets ACLs for you.
When to skip the middleware
Three cases where adding the middleware does not buy you anything:
- Blazor Server. Blazor Server renders the bulk of the page over a persistent SignalR connection, not as HTTP responses the middleware can rewrite. The image and CSS optimization still applies to the initial document and any static assets, but Interaction to Next Paint on a Blazor Server application is dominated by SignalR round-trip latency, which the middleware cannot touch. Architectural improvements (Blazor WebAssembly, Blazor Auto, or moving expensive interactions client-side) are the right tool there.
-
Pure JSON APIs. If the application only emits
application/jsonfrom controller endpoints, there is nothing for the middleware to rewrite. The optimizations apply to HTML, CSS, JavaScript, and image responses. Run it for the static-asset side only if the API has a companion documentation site. - You already run a CDN with image-optimization features turned on. Two image optimizers in sequence usually overlap without benefit. Pick one layer (either the CDN or the middleware) and run the other in pass-through.
Pure-Kestrel deployment
The same registration works unchanged when you take IIS out of the
picture entirely: Kestrel directly on a Linux container, fronted by
nginx as a reverse proxy, in Docker on a developer workstation.
That portability is part of the reason ASP.NET Core teams pick
middleware over the IIS-module path: the same optimization runs in
every environment the application runs in, including the local
dotnet run loop.
See the
ASP.NET
Core middleware deep-dive on modpagespeed.com for the longer
write-up: performance characteristics, the P/Invoke layer that
bridges the C# pipeline to the native PageSpeed Automatic library,
and the configuration shape under
appsettings.json.
Where to start
The
WeAmp.PageSpeed.AspNetCore
NuGet package is the starting point: install it and run it — the
line is open source under Apache-2.0, free in development and in
production, on every platform you deploy to. What We-Amp sells is
support. Existing
IISpeed customers
moving to ASP.NET Core transfer their license at no cost through the
license-transfer
form.
Add WeAmp.PageSpeed.AspNetCore to an ASP.NET Core app
Add two lines to Program.cs and run it — the line is
open source under Apache-2.0, free in development and in production;
support plans are available.
Related
- IISpeed transition overview — what changed, how the license transfers, and where the optimization library lives now.
- IIS image optimization with WebP and AVIF — the classic-IIS path alongside the ASP.NET Core path.
- IIS Core Web Vitals in 2026 — measurement workflow and the filters that move each metric.
- ASP.NET Core middleware deep-dive — how the NuGet package wraps the PageSpeed Automatic library.
- ASP.NET Core image optimization in C# — a worked example with measurements.
- IISpeed successor: mod_pagespeed for IIS — the canonical successor page.