Working with Save-Data header in ASP.NET Core
February 03, 2019 by Anuraj
ASP.NET Core
This post is about working with Save-Data header in ASP.NET Core. The Save-Data client hint request header available in Chrome, Opera, and Yandex browsers lets developers deliver lighter, faster applications to users who opt-in to data saving mode in their browser.
One fairly straightforward technique is to let the browser help, using the Save-Data request header. By identifying this header, a web page can customize and deliver an optimized user experience to cost- and performance-constrained users.
For this post, a middleware is getting implemented, which is looking for Image files(JPG), and it save-data header exists, it is compressing the images and delivering it.
public class SaveDataMiddleware
{
private readonly RequestDelegate _next;
private readonly IHostingEnvironment _hostingEnvironment;
private static readonly string[] suffixes = new string[] {
".jpg",
".jpeg"
};
public SaveDataMiddleware(RequestDelegate next, IHostingEnvironment hostingEnvironment)
{
_next = next;
_hostingEnvironment = hostingEnvironment;
}
public async Task Invoke(HttpContext httpContext)
{
var path = httpContext.Request.Path;
if (!IsImagePath(path))
{
await _next.Invoke(httpContext);
return;
}
var isSaveDataEnabled = false;
if (httpContext.Request.Headers.TryGetValue("save-data", out StringValues saveDataHeaders))
{
isSaveDataEnabled = saveDataHeaders.Count == 1 &&
saveDataHeaders[0].Equals("on", StringComparison.OrdinalIgnoreCase);
}
if (isSaveDataEnabled)
{
var imagePath = Path.Combine(_hostingEnvironment.WebRootPath,
path.Value.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar));
using (var image = Image.FromFile(imagePath))
{
using (var lowQualityImage = new Bitmap(image.Width, image.Height))
{
using (var graphics = Graphics.FromImage(lowQualityImage))
{
graphics.InterpolationMode = InterpolationMode.Low;
graphics.SmoothingMode = SmoothingMode.HighSpeed;
graphics.CompositingQuality = CompositingQuality.HighSpeed;
graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
var imageRectangle = new Rectangle(0, 0, image.Width, image.Height);
graphics.DrawImage(image, imageRectangle);
using (var memoryStream = new MemoryStream())
{
lowQualityImage.Save(memoryStream, image.RawFormat);
httpContext.Response.ContentLength = memoryStream.Length;
await httpContext.Response.Body.WriteAsync(memoryStream.ToArray(), 0, (int)memoryStream.Length);
}
}
}
}
}
else
{
await _next(httpContext);
}
}
private bool IsImagePath(PathString path)
{
if (path == null || !path.HasValue)
{
return false;
}
return suffixes.Any(x => x == Path.GetExtension(path.Value));
}
}
And next you can configure extension method to add it to http pipeline.
public static class SaveDataMiddlewareExtensions
{
public static IApplicationBuilder UseSaveDataMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SaveDataMiddleware>();
}
}
You need to add this middleware before StaticFilesMiddleware, otherwise it won’t work properly.
And you can verify this implementation using Data Saver chrome extension.
If Data-Saver mode is enabled, browser will send a HTTP Request header.
The page display 5 images. And here is the network tab, without save-data mode.
And here is Network tab, after enabling the save-data
header.
In this post, I am only handling JPG images, you can also exclude stylesheets and fonts etc, if it is not critical for the page. You can get more details about save-data header and various implementations here - Delivering Fast and Light Applications with Save-Data
Happy Programming :)
Copyright © 2025 Anuraj. Blog content licensed under the Creative Commons CC BY 2.5 | Unless otherwise stated or granted, code samples licensed under the MIT license. This is a personal blog. The opinions expressed here represent my own and not those of my employer. Powered by Jekyll. Hosted with ❤ by GitHub