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.
EnableForHttpsis 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:
UseResponseCompression()— outermost, so it compresses the final outputUseStaticFiles()with the cache headersUseSession(),UseRouting()UseWebMarkupMin()— production only, inside compressionMapRazorPages()
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.