Using WebP Images in APS.NET with a fallback to JPEG and PNG
WebP images in ASP.NET with fallback for browsers not supporting WebP
WebP is an image format introduced by Google Developers and it provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.
WebP lossless images support transparency which makes them ideal replacement for PNG image format. In addition images saved in WebP image format are about 26% smaller in size compared to PNG. WebP lossy images are 25–34% smaller than comparable JPEG images. Some of the CDN providers like CloudFlare recognized potential of WebP image format and it's role in lowering down bandwidth and reducing page speed load time, so they introduced support for automatic WebP compression by detecting browser ability to support WebP image format. You can read more about this at CloudFlare blog https://blog.cloudflare.com/a-very-webp-new-year-from-cloudflare/
So if you are willing to pay for CloudFlare CDN, you have the ability to serve WebP images automatically from your website. In case you are not using the CDN, and you serve your static image content from your host, proceed reading this article.
However, despite all the features it is not widely supported by all browsers. It is fully supported only by Google Chrome. Full support list can be fond at https://caniuse.com/webp
You can see that as of now only Chrome is fully supporting it, but it still makes around 75% of global browser usage. If you want to fully support WebP with a fullback to png or jpeg you need to to a workaround which is basically based on browser detection and serving different image based on the visitors browser.
The following table is a basic guideline how you can detect browser based on the user agent.
Browser name | Must contain | Must not contain |
Firefox | Firefox/xyz | Seamonkey/xyz |
Seamonkey | Seamonkey/xyz | |
Chrome | Chrome/xyz | Chromium/xyz |
Chromium | Chromium/xyz | |
Safari | Safari/xyz | Chrome/xyz Chromium/xyz |
Opera | OPR/xyz Opera/xyz | |
Internet Explorer | ; MSIE xyz; |
Browser detection and content serving
If you are running your website on ASP.NET you can do the detection and content selection for serving in and IHttpHanlder interface class implementation. I have wrote the following basic IHttpHandler which will serve either WebP or PNG or JPEG image as a fallback depending which one is present in the same folder as webp file.
using System; using System.Web; using System.IO; namespace Web.Images.WebP { public class RequestHandler : IHttpHandler { public bool IsReusable => true; public void ProcessRequest(HttpContext context) { if (context.Request.Url.AbsoluteUri.EndsWith(".webp", StringComparison.InvariantCultureIgnoreCase)) { var path = context.Server.MapPath(context.Request.Url.AbsolutePath); if (context.Request.UserAgent.IndexOf("Chrome/", StringComparison.InvariantCultureIgnoreCase) >= 0) { context.Response.ClearHeaders(); context.Response.ClearContent(); if (File.Exists(path)){ var content = File.ReadAllBytes(path); context.Response.OutputStream.Write(content, 0, content.Length); context.Response.OutputStream.Flush(); context.Response.AppendHeader("Content-type","image/webp"); } else { ImageFallback(context,path); } } else { ImageFallback(context,path); } } } private String GetImagePath(String path, String extension) { return Path.Combine(Path.GetDirectoryName(path), String.Concat(Path.GetFileNameWithoutExtension(path),".", extension)); } private void ImageFallback(HttpContext context,String path) { var extensions = new String[] { "png","jpg","jpeg" }; bool found = false; foreach(var extension in extensions) { var imagePath = GetImagePath(path, extension); if (File.Exists(imagePath)) { var staticUrl = context.Request.Url.AbsoluteUri.Substring(0, context.Request.Url.AbsoluteUri.LastIndexOf("/")); staticUrl = String.Concat(staticUrl,"/", Path.GetFileName(imagePath)); found = true; context.Response.Redirect(staticUrl); } } if (!found) { context.Response.ClearContent(); context.Response.ClearHeaders(); context.Response.StatusCode = 404; } } } }
This handler requires you to have same image in webp and jpeg or png in the same folder, for example if you have file in c:\inetpup\wwroot\website1\images\image.webp you will need to have an c:\inetpup\wwroot\website1\images\image.png or c:\inetpup\wwroot\website1\images\image.jpg as well in order for fallback to work.
For the fallback, we are just doing redirection to static image, as we do not really need to read the content of the fallback (jpg or png) file iself, redirection will do the trick.
To involve your handler in you request pipeline, you need to register it in web.config, handlers section
<configuration> <system.webServer> <handlers> <remove name="WebPHandler" /> <add name="WebPHandler" type="Web.Images.WebP.RequestHandler, Web.Images.WebP" path="*.webp" verb="GET" /> </handlers> </system.webServer> </configuration>
Now if you try to access webp image from Firefox you will get the fallback jpg or png while accessing the webp from Chrome will give you webp image content
If you have tons of file you want to convert to WebP you can check Converting existing website images to Google WebP using PowerShell article which you can use to convert your images to webp with keeping the original image in the same folder so you can easily apply this http handler class in your ASP.NET web application
Performance improvements
The solution above works but it has some drawbacks. For every webp image request we are doing file read. It is essentially IO operation, so for hight traffc websites this can cause an issue by putting higher pressure on the disk IO Read operations.
Another thing for fallback scenario is also IO operation of checking the file existence, which is for sure les resource and time consuming then file reading but it is essentially an IO operation.
For this purpose I added one more class which will store cached webp image content along with the fallback image URL so we do not need to execute any IO call on the visits except only the first one.
using System; namespace Web.Images.WebP { internal class ImageCacheModel { public byte[] WebpContent { get; set; } public String FallbackImage { get; set; } } }
Now we need to do some refactoring on HttpHandler class itself to read from cache rather than the local storage
using System; using System.Web; using System.IO; using System.Web.Caching; namespace Web.Images.WebP { public class RequestHandler : IHttpHandler { public bool IsReusable => true; public void ProcessRequest(HttpContext context) { var imageCacheItem = GetFromCache(context); if (context.Request.Url.AbsoluteUri.EndsWith(".webp", StringComparison.InvariantCultureIgnoreCase)) { var path = context.Server.MapPath(context.Request.Url.AbsolutePath); if (context.Request.UserAgent.IndexOf("Chrome/", StringComparison.InvariantCultureIgnoreCase) >= 0) { context.Response.ClearHeaders(); context.Response.ClearContent(); if (imageCacheItem != null && imageCacheItem.WebpContent != null) { ReturnWebpContent(context, imageCacheItem.WebpContent); } else { if (File.Exists(path)) { var content = File.ReadAllBytes(path); UpdateCacheContent(context, content, path); ReturnWebpContent(context, content); } else { ImageFallback(context, path); } } } else { ImageFallback(context,path); } } } private void ReturnWebpContent(HttpContext context, byte[] content) { context.Response.OutputStream.Write(content, 0, content.Length); context.Response.OutputStream.Flush(); context.Response.AppendHeader("Content-type", "image/webp"); } private String GetImagePath(String path, String extension) { return Path.Combine(Path.GetDirectoryName(path), String.Concat(Path.GetFileNameWithoutExtension(path),".", extension)); } private void ImageFallback(HttpContext context,String path) { var imageCacheItem = GetFromCache(context); if (imageCacheItem != null && !String.IsNullOrWhiteSpace(imageCacheItem.FallbackImage)) { context.Response.Redirect(imageCacheItem.FallbackImage); } else { var extensions = new String[] { "png", "jpg", "jpeg" }; bool found = false; foreach (var extension in extensions) { var imagePath = GetImagePath(path, extension); if (File.Exists(imagePath)) { var staticUrl = context.Request.Url.AbsoluteUri.Substring(0, context.Request.Url.AbsoluteUri.LastIndexOf("/")); staticUrl = String.Concat(staticUrl, "/", Path.GetFileName(imagePath)); found = true; UpdateCacheFallback(context, staticUrl, path); context.Response.Redirect(staticUrl); } } if (!found) { context.Response.ClearContent(); context.Response.ClearHeaders(); context.Response.StatusCode = 404; } } } #region Cache handling private ImageCacheModel GetFromCache(HttpContext context) { return context.Cache.Get(context.Request.Url.AbsoluteUri.ToLower()) as ImageCacheModel; } private void UpdateCacheContent(HttpContext context, byte[] content, String filePath) { var imageCacheItem = GetFromCache(context); if (imageCacheItem == null) { imageCacheItem = new ImageCacheModel(); } imageCacheItem.WebpContent = content; context.Cache.Insert(context.Request.Url.AbsoluteUri.ToLower(), imageCacheItem, new CacheDependency(filePath)); } private void UpdateCacheFallback(HttpContext context, String fallbackUrl, String filePath) { var imageCacheItem = GetFromCache(context); if (imageCacheItem == null) { imageCacheItem = new ImageCacheModel(); } imageCacheItem.FallbackImage = fallbackUrl; context.Cache.Insert(context.Request.Url.AbsoluteUri.ToLower(), imageCacheItem, new CacheDependency(filePath)); } #endregion } }
Now content will be served from cache instead of going to the disk for every image request.
This solution will work only for the scenario where your images are hosted on the same storage as the web application. In case you are hosting your images on the CDN this will not work
References
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
- https://developers.google.com/speed/webp/
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article