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

ModPageSpeed 2.0 now works with ASP.NET Core

By Otto van der Schaaf

architecture announcement

ModPageSpeed 2.0 has been nginx-only until now. The nginx interceptor does zero-copy cache serving, 103 Early Hints, and sub-millisecond variant selection — but it requires nginx.

If your application runs on ASP.NET Core, adding nginx as a reverse proxy just to get page optimization is a significant architectural change. You inherit nginx’s configuration language, its process model, and its deployment story.

The ASP.NET Core middleware is a second integration path: same C++ optimization pipeline, called from your app through a NuGet package.

Install

Two packages — the middleware itself, and the native assets for your runtime.

dotnet add package WeAmp.PageSpeed.AspNetCore
dotnet add package WeAmp.PageSpeed.NativeAssets.Linux      # linux-x64
# or: dotnet add package WeAmp.PageSpeed.NativeAssets.macOS   # osx-arm64
# or: dotnet add package WeAmp.PageSpeed.NativeAssets.Windows # win-x64

The managed packages are platform-independent. The NativeAssets package bundles libpagespeed and the factory_worker binary for one runtime identifier. Pick the one matching your deployment target, or reference more than one if you ship cross-platform.

Wire it up

A minimal 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.Worker.SocketPath = "/data/pagespeed.sock";
});

var app = builder.Build();

app.UsePageSpeed();
app.UseStaticFiles();
app.UseRouting();

app.Run();

AddPageSpeed() also takes an IConfiguration overload that binds to a PageSpeed section in appsettings.json — see ASP.NET Core Getting Started for the JSON layout. Options hot-reload through IOptionsMonitor<PageSpeedOptions>.

Standalone vs full stack

The middleware supports two deployment modes.

Standalone — no worker, no image optimization. The middleware processes HTML only: critical CSS inlining, lazy-load attributes, explicit image dimensions, LCP preload hints, preconnect hints for third-party origins. Set Worker.SocketPath to null (or omit it) and the middleware operates independently. Useful when image optimization already lives elsewhere — a CDN, a build step — and you just want HTML processing.

Full stack — ASP.NET middleware plus worker. The middleware handles HTML processing and cache serving. The worker handles image transcoding (WebP, AVIF, SVG), viewport-based resizing, Save-Data quality variants, and pre-compressed alternates (gzip, brotli). Same optimization pipeline as the nginx integration, with the same cache format and the same variant selection.

For the full stack the worker runs alongside your ASP.NET app and shares a Cyclone cache file over a volume:

services:
  worker:
    build:
      target: worker-runtime
    volumes:
      - shared:/shared

  aspnet:
    build:
      target: aspnet-runtime
    environment:
      PAGESPEED_CACHE_PATH: /shared/cache.vol
      PAGESPEED_SOCKET_PATH: /shared/pagespeed.sock
    volumes:
      - shared:/shared
    depends_on:
      - worker

volumes:
  shared:

Both processes open the cache file via memory-mapped I/O. The middleware writes originals, the worker writes optimized variants, and each side sees the other’s writes immediately.

cd samples/aspnetcore/samples/DemoSite
./run-demo.sh               # Full stack: ASP.NET + worker
./run-demo.sh --standalone  # HTML processing only, no worker

How a request flows

  1. Classify. The middleware reads Accept, User-Agent, Save-Data, and Accept-Encoding and calls into the native library to produce a 32-bit capability mask. Same classification as the nginx interceptor.

  2. Cache lookup. ReadBest(url, hostname, scheme, mask) on the shared Cyclone cache returns the best matching variant, or nothing.

  3. HIT. The cached content is served directly. The middleware reads any stored early hints (preload URLs, preconnect origins) and adds them as Link response headers. Response gets X-PageSpeed: HIT.

  4. MISS. The middleware lets the request proceed through the rest of the pipeline. It captures the response body, checks that it is HTML within size limits, and runs it through the native HTML processor. The processor extracts critical CSS, adds lazy-load attributes, injects image dimensions, and collects preload hints. The processed HTML and the early hints are written to the cache, and the worker is notified asynchronously to generate image variants and compressed alternates.

  5. Worker notification. A bounded channel queues notifications without blocking the request path. The background service drains the channel and sends each notification to the worker over a Unix socket. In standalone mode the channel drains into a no-op.

Nginx vs middleware

Nginx interceptorASP.NET middleware
Cache servingZero-copy via mmap’d ngx_buf_tMemory copy into response stream
103 Early HintsSent before proxying on MISSNot supported (Kestrel limitation)
IntegrationReverse proxy in front of originNuGet package in your pipeline
Configurationnginx.conf directivesC# options / appsettings.json
DeploymentSeparate nginx processSame process as your app
HTML processingSame pipelineSame pipeline
Image optimizationSame workerSame worker
Cache formatSame Cyclone cacheSame Cyclone cache
Variant selectionSame 32-bit maskSame 32-bit mask
RecompilationRequires nginx headersdotnet add package

Nginx still serves cache hits faster. Zero-copy mmap means cached variants go from disk to socket with no intermediate buffer; the middleware copies through the managed-memory pipeline. The nginx module also sends 103 Early Hints on cache misses — Kestrel doesn’t expose that to middleware yet.

The middleware’s trade is integration. A NuGet package, two lines of C#, configuration in appsettings.json. No separate process, no nginx.conf, no reverse proxy.

Both paths share the optimization pipeline, the cache, the worker, and the variant logic. Only the serving layer differs.

Other languages

The C API is plain. libpagespeed.so exposes a stable set of functions — classify a request, read from cache, process HTML, notify the worker — that anything with FFI can call. ASP.NET Core is the first non-nginx integration, not the last; a Go middleware, a Rust middleware, or a Python WSGI wrapper would call the same functions.


Once it’s running, append ?bypass=1 to any URL to compare the optimized output against the original.

For current installation instructions and configuration reference, see ASP.NET Core Getting Started.

Read next