Skip to main content

ASP.NET Core on IIS: the WeAmp.PageSpeed shape

Updated 2026-05-20 · 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 1.1 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 middleware sibling of mod_pagespeed 1.1. Same PageSpeed Automatic optimization library underneath, 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

using WeAmp.PageSpeed.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPageSpeed(options =>
{
    options.RewriteLevel = "CoreFilters";
    options.EnableFilters = new[]
    {
        "recompress_images",
        "convert_jpeg_to_webp",
        "resize_images",
        "insert_image_dimensions",
        "lazyload_images",
        "prioritize_critical_css",
        "defer_javascript",
        "combine_javascript",
        "extend_cache",
    };
    options.FileCachePath = @"C:\ProgramData\WeAmp.PageSpeed\cache";
    options.FileCacheSizeKb = 1_048_576;
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

// Order matters: UsePageSpeed before UseStaticFiles so the middleware
// can intercept static-asset responses; before MapControllers so it
// can rewrite HTML responses from the application.
app.UsePageSpeed();
app.UseStaticFiles();
app.MapControllers();
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, file downloads, anything the application explicitly marks no-rewrite) skip the optimization layer.

Filters worth knowing about

The middleware exposes the same filter set as mod_pagespeed 1.1. Five do most of the Core Web Vitals work:

  • recompress_images, convert_jpeg_to_webp, and resize_images: the image trio. The middleware generates a WebP variant of each JPEG and serves it via Accept negotiation, recompresses oversized PNGs, and resizes images to the dimensions the HTML actually declares. Hero-image weight typically drops by an order of magnitude on image-heavy Razor pages.
  • prioritize_critical_css: extracts the above-the-fold CSS rules and inlines them in the document head, deferring the rest. First paint stops blocking on a stylesheet round trip.
  • defer_javascript and combine_javascript: move non-critical JavaScript out of the critical path and reduce the number of round trips to fetch script bundles. 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. Server-push hints are emitted via Link: rel=preload headers, which IIS forwards untouched.
  • URL Rewrite Module. Any URL rewrite rules in web.config execute 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 directory. The middleware writes optimized variants to the configured FileCachePath. The path must be writable by the application-pool identity (commonly IIS APPPOOL\<site>). The default location under C:\ProgramData\WeAmp.PageSpeed\cache is created with the right ACLs by the NuGet package's first-run setup.

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/json from 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 Preview today. Install it and run unlicensed to evaluate: it fully optimizes and just adds an X-PageSpeed-Warn: unlicensed header. When you are ready for production, buy a license — an immediate-charge subscription via FastSpring, and the same license covers the middleware on every platform you deploy to. A commercial license is required for production use. Existing IISpeed customers moving to ASP.NET Core can apply the IISpeed license transfer at no cost against the middleware as well as the 1.1 IIS module.

Add WeAmp.PageSpeed to an ASP.NET Core app

Add two lines to Program.cs and run unlicensed to evaluate — it optimizes regardless, adding an X-PageSpeed-Warn: unlicensed header until you buy. A commercial license is required for production use.

Related