Skip to main content
ModPageSpeed 2.0: AVIF, WebP, and critical CSS — up to 69% less page weight on the live demo
2.0 1.1

Install ASP.NET Core middleware

Install ModPageSpeed as ASP.NET Core middleware via NuGet. Same optimization pipeline, no nginx required.

ModPageSpeed runs inside your ASP.NET Core pipeline as middleware, installed as a NuGet package. Same optimization pipeline as the nginx integration — image transcoding, CSS/JS minification, critical CSS — invoked via P/Invoke to the native C/C++ library. No separate reverse proxy or sidecar is required.

Prerequisites

  1. .NET 8 SDK or .NET 10 SDKdotnet.microsoft.com. The package targets net8.0 and net10.0.
  2. Supported platform: Linux x64 or arm64, macOS ARM64 (Apple Silicon), or Windows x64
  3. An ASP.NET Core application that serves HTML responses (Razor Pages, MVC, Blazor Server, or minimal APIs returning HTML)

Install the package

A single dotnet add command is enough — the meta-package pulls in the core abstractions and all native-asset packages transitively.

dotnet add package WeAmp.PageSpeed.AspNetCore

Three native-asset packages ship alongside the managed assemblies — one each for linux-x64/linux-arm64, osx-arm64, and win-x64. NuGet pulls all of them as transitive dependencies; MSBuild’s RID resolution loads only the matching platform’s binary at publish/run time.

Add to your pipeline

Register PageSpeed services and add the middleware to your request pipeline.

using WeAmp.PageSpeed.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Register PageSpeed services — auto-binds the "PageSpeed" section from
// IConfiguration (appsettings.json, env vars, user secrets, …).
builder.Services.AddPageSpeed();

var app = builder.Build();

// Add PageSpeed middleware — should be early in the pipeline, before
// anything that writes the response body.
app.UsePageSpeed();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();

Place UsePageSpeed() before any middleware that writes the response body. It needs to intercept responses before they reach the client.

That is the whole setup. There is no Worker section to configure. The worker process starts automatically and coordinates over an auto-resolved per-process socket, so HTML and image optimization are on out of the box. You only add a PageSpeed:Worker section when you want to change that behavior; see the configuration reference.

Configure via appsettings.json

Add a PageSpeed section to your appsettings.json. A minimal configuration:

{
  "PageSpeed": {
    "Enabled": true
  }
}

That’s enough to turn on every default rewrite (critical-CSS inlining, LCP preload, lazy loading, image dimensions, async CSS, script deferral, …). Each rewrite is its own boolean toggle under PageSpeed:Html — see the configuration reference for the full list.

Verify the install

Run your application:

dotnet run

Check the terminal output for the URL your app is listening on (e.g. http://localhost:5123). Three checks confirm the install end to end, from quickest to most thorough.

1. The X-PageSpeed header on a content route

curl -i http://localhost:<port>/

Look for the X-PageSpeed header on HTML responses. It reports the cache outcome:

X-PageSpeed: MISS

or, once the worker has built the variant:

X-PageSpeed: HIT

MISS means the worker is generating the optimized variant in the background; you’ll see HIT on the next request. If the header is present, PageSpeed is active. Check a content route like / or one of your assets — the /console/* routes are short-circuited before the middleware, so they intentionally do not carry X-PageSpeed.

2. The dashboard shows activity

Open http://localhost:<port>/console/dashboard. After a few requests, the Dashboard and Metrics show non-zero request and optimization counts. If they read zero, jump to My dashboard shows zeros.

3. Content negotiation serves smaller images

We don’t rewrite URLs in 2.0 — the same /hero.jpg URL serves WebP to WebP-capable browsers and AVIF to AVIF-capable ones. The worker selects the variant from the request Accept header, and the response carries Vary: Accept, Save-Data, User-Agent so caches keep the variants apart:

curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/jpeg'
# Content-Length: 98230   Content-Type: image/jpeg   Vary: Accept, Save-Data, User-Agent

curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/webp'
# Content-Length: 2422    Content-Type: image/webp   Vary: Accept, Save-Data, User-Agent

curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/avif'
# Content-Length: 415     Content-Type: image/avif   Vary: Accept, Save-Data, User-Agent

The same URL returns smaller bytes per format. (Byte counts are from one sample image; yours will differ.) View the HTML source to see the rest: inlined critical CSS, lazy-loaded images, and deferred scripts.

Coming from 1.x? There are no .pagespeed. magic URLs in 2.0. The HTML stays clean and the original URLs serve optimized bytes through content negotiation. If you’re looking for rewritten asset URLs in the page source, you won’t find them — that’s expected.

My dashboard shows zeros and nothing is moving

If the Dashboard reads zero and DevTools shows no smaller image variants, the worker isn’t coordinating with the middleware. On releases before 2.0.14, Worker.SocketPath defaulted in a way that could leave coordination off; from 2.0.14 the default is an auto-resolved per-process socket with coordination on, so the quickstart above just works.

Check, in order:

  1. You’re on 2.0.14 or newerdotnet list package should show WeAmp.PageSpeed.AspNetCore at 2.0.14+.
  2. You haven’t set PageSpeed:Worker:SocketPath to null or "" in appsettings.json or in code. Either value disables coordination and logs a startup warning — leave the key unset for the default. See Worker configuration.
  3. PageSpeed:Worker:AutoStart is still true (the default). With it false, no worker process launches at all.

Then re-run the content-negotiation check — a webp request should return materially fewer bytes with Content-Type: image/webp.

Known limitations

  • License state changes propagate to the middleware within 30 seconds (health poll interval)

Next steps

  • Configuration reference — all appsettings.json options, hot reload, environment-specific config
  • Licensing — Commercial subscription today; BSL 1.1 source publication on the roadmap