.NET August 16, 2026 GlobalDigitaz Engineering 0 comments

Brotli, HTML Minification and Cache Headers: A Faster ASP.NET Core Site

The four server-side changes that cut page weight and repeat-visit load time on an ASP.NET Core site — response compression, production-only HTML minification, versioned assets and long cache headers.

Front-end work dominates most performance conversations, and rightly so. But there are four server-side changes in ASP.NET Core that take an afternoon, carry almost no risk, and reduce what you send over the wire by most of its original size. Here they are, in the order we apply them.

1. Response compression — Brotli first, gzip as fallback

Text responses — HTML, CSS, JavaScript, JSON, SVG, XML — compress extremely well. Brotli generally beats gzip by a further 15–20% on the same content, and every current browser supports it.

builder.Services.AddResponseCompression(o =>
{
    o.EnableForHttps = true;
    o.Providers.Add<BrotliCompressionProvider>();
    o.Providers.Add<GzipCompressionProvider>();
    o.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
    {
        "image/svg+xml", "application/json", "application/xml", "text/xml"
    });
});

Then register it first in the pipeline, so it wraps everything written below it:

app.UseResponseCompression();

Two things worth knowing

  • Do not add fonts or images to the MIME list. WOFF2, JPEG, PNG and WebP are already compressed. Running Brotli over them burns CPU for roughly nothing.
  • EnableForHttps is off by default, and that default exists for a reason: compressing a response that mixes a secret with attacker-influenced input can leak the secret through response size. For a public marketing site serving public content, turning it on is standard. For an authenticated app handling sensitive data, think it through first.

2. HTML minification — production only

Razor output carries a lot of indentation, blank lines and comments. Stripping them costs nothing at runtime and shrinks every HTML response, and because compression sits outside minification in the pipeline, the two compound.

The important part is keeping it out of Development. Nobody wants to debug a single-line document in View Source:

if (!app.Environment.IsDevelopment())
{
    app.UseWebMarkupMin();
}

Be conservative with the settings. Whitespace removal in Safe mode and comment stripping are free wins. Removing optional end tags and stripping protocols from URLs is where minifiers break hand-written markup — canonical links and og:url values have to stay absolute.

3. Version your static assets

This is the step that makes step four safe. Every local stylesheet, script and image URL carries a token derived from the file itself:

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />

Change the file, the token changes, the URL changes, browsers fetch the new bytes. Leave the file alone and the URL is stable forever. Without this, aggressive caching means visitors keep a stale stylesheet until they hard-refresh — which they will not do.

4. Cache headers that reflect reality

Once assets are versioned you can cache them for a year with confidence. Types that are never edited in place get the strongest headers; anything that might be overwritten has to revalidate:

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        var ext = Path.GetExtension(ctx.File.Name);
        var isVersioned = ctx.Context.Request.Query.ContainsKey("v");

        if (ImageAndFontTypes.Contains(ext))
            ctx.Context.Response.Headers.CacheControl = isVersioned
                ? "public,max-age=31536000,immutable"
                : "public,max-age=31536000";
        else if (ext is ".css" or ".js")
            ctx.Context.Response.Headers.CacheControl = isVersioned
                ? "public,max-age=31536000,immutable"
                : "no-cache";
    }
});

The immutable directive is the meaningful part: it tells the browser not even to send a conditional request on reload. A returning visitor downloads the HTML and nothing else.

Never put a long max-age on an unversioned CSS or JS file. You will ship a fix and a portion of your audience will not see it for a year.

Order matters

The pipeline has to nest correctly or you will compress un-minified HTML, or minify already-compressed bytes:

  1. UseResponseCompression() — outermost, so it compresses the final output
  2. UseStaticFiles() with the cache headers
  3. UseSession(), UseRouting()
  4. UseWebMarkupMin() — production only, inside compression
  5. MapRazorPages()

What this does not fix

Compression and caching reduce bytes and repeat-visit cost. They do not fix a slow query, a render-blocking third-party script or a 2 MB hero image. If your Largest Contentful Paint is four seconds, the problem is almost certainly in the browser, not the pipeline — see Core Web Vitals for ASP.NET Core sites for where that time actually goes.

On the data side, query cost is the other half of a predictable time-to-first-byte, which is part of why we use Dapper over Entity Framework on read-heavy projects.

We do this work as part of every .NET Core development engagement, and it is usually the first thing we change on an inherited codebase. If you are a .NET developer who cares about this kind of detail, we are hiring. Otherwise, send us a URL and we will tell you what is slow.

Found this helpful?
Share it with your team or start a project with us.

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Never displayed publicly.
Captcha
Look at the image and type the math answer.

Comments are moderated and approved before appearing publicly.

About the Author

G
GlobalDigitaz Engineering
GlobalDigitaz Team

Our engineering team shares insights from building 500+ real-world projects across web, mobile, cloud, and enterprise software.

Start Your Project

Free consultation · Proposal in 24 hours · No commitment.

Get in Touch