Dynamically Resize Your ASP.NET MVC Images
After the redesign, there was an issue with images on DanylkoWeb. Today, I show you a quick and dirty way to dynamically resize images in ASP.NET MVC.
With the new redesign behind me, I decided to check out some performance metrics to see how fast the site performed.
I decided to test it out on GTMetrix.com.
Hmm...Ouch.
After looking over the results, I noticed the six images from the blog posts were dragging it down giving me a 'B' (better than an 'F', I guess).
My problem was delivering the full 640x480 blog post images to the browser, defining a constrained area where I shove an image into a DIV, and let the container/Bootstrap take care of everything for me since it handles responsive so well.
The problem with this thinking is the amount of bytes transferred over the wire to the browser. You need to shrink the images to the correct dimensions before it's sent to the browser.
This will give you an increase in performance by sending less bytes to the browser making your users even happier.
You could achieve this by one of two ways:
- Use a task runner to resize all of the images and store them in a thumbnail folder before deployment
- Dynamically shrink the images when requested
Since I didn't want to tie up more storage on a hosted server, I decided to implement the latter.
An Image Request Is Still A Request
We can use controllers to retrieve a trimmed down version of an image.
In the image tag, we define the source as a Url.Action pointing to a controller action that returns a resized version of our image.
All we need is the name of the file along with an ImageResult ActionResult.
First, we start with our controller action.
public ActionResult Thumbnail(string filename) { var img = new WebImage(filename) .Resize(291, 285, false, true); return new ImageResult(new MemoryStream(img.GetBytes()), "binary/octet-stream"); }
Since I only want to resize the home page images for now, I placed mine on my Home controller. You could even create a Services controller and place it there.
We also need our image dimensions for our image tag so I'll hard code these and refactor later.
I also want to bring to your attention a WebHelper class called WebImage.
This WebHelper provides the basics for manipulating images dynamically. While I'm only resizing the image, the WebImage class can:
- Add an image or text watermark
- Clone
- Crop
- Flip Horizontally/Vertically
- Rotate Left/Right
It uses a fluent syntax so you can Resize, Flip Horizontally, and add a text watermark before you send the image back.
ImageResult ActionResult
The ImageResult is a simple ActionResult and returns back a binary/octet-stream.
public class ImageResult : ActionResult { public ImageResult(Stream imageStream, string contentType) { if (imageStream == null) throw new ArgumentNullException("imageStream"); if (contentType == null) throw new ArgumentNullException("contentType");
this.ImageStream = imageStream; this.ContentType = contentType; }
public Stream ImageStream { get; set; } public string ContentType { get; set; }
public override void ExecuteResult(ControllerContext context) { if (context == null) throw new ArgumentNullException("context");
var response = context.HttpContext.Response;
response.ContentType = this.ContentType;
byte[] buffer = new byte[4096]; while (true) { int read = this.ImageStream.Read(buffer, 0, buffer.Length); if (read == 0) break;
response.OutputStream.Write(buffer, 0, read); }
response.End(); } }
To optimize our performance, it's best to use streams when sending bytes through the pipe.
I've seen people load the image into memory and then send it out. Writing out to the OutputStream provides a faster way to deliver images.
What does the Razor syntax look like?
The Razor syntax may look a little strange, but it's a quick and dirty way of dynamically returning images.
<img src="@Url.Action("Thumbnail", "Home", new {FileName="~/Content/Images/Cop.jpg"})" class="img-fluid" title="@post.Title" alt="@post.Title" width="291" height="285" />
Simple, right?
I also found out that I get 'dinged' on points when I don't specify an image's dimensions for the browser. This way, it defines the area and then fills in the image from the stream.
If I don't have the dimensions, the browser has to wait for the image to completely download delaying the rendering of the layout.
Can we make it even faster?
After thinking about this a little more, we can add even more performance to the site.
Since a controller can use ActionFilters, we can apply caching to the Action as well.
[ETag, OutputCache(Duration = 3600, VaryByParam = "filename")] public ActionResult Thumbnail(string filename) { var img = new WebImage(filename) .Resize(291, 285, false, true); return new ImageResult(new MemoryStream(img.GetBytes()), "binary/octet-stream"); }
The OutputCache is set for 1 hour and will cache the result by filename.
The ETag is an ActionFilter used to determine whether an asset requires updated or not. I've talked about this performant ActionFilter as one of my favorite ASP.NET MVC ActionFilters.
[UPDATES 2018-Jan-06]: As noted in the comments below, images weren't appearing on the main page. After researching (and digging into Chrome Dev Tools), this happened for two reasons:
- The Controller's Action was synchronous. When trying to convert a number of images, it's best to make the resizing asynchronous. As soon as I converted the method over to asynchronous, the images would appear.
- I removed the ETag, but kept the OutputCache to cache the images. The images don't require an ETag.
Did it work?
After I put everything in place, I went back to GTMetrix.com and ran another test.
Not bad.
I saw all green on my results, my page load time decreased as well as my Total Page Size, but my requests went up a little. Yikes!
Also, I'm .9 milliseconds off from losing my audience according to web statistics (40% abandon a website that takes longer than 3 seconds to load).
Of course, I will be spending a little more time to get that .9 milliseconds down.
Conclusion
Today, I presented a way to scale your images down to a moderate size using an ImageResult and a WebImage class.
You should always be optimizing your site for the best possible performance you can squeeze out of your code.
Images are just one aspect of optimizing your site.
I'll talk more about optimizing your ASP.NET MVC site in the future. If you have certain performance questions, please post them in the comments below.
Post your comments below about how to shift your MVC site into high gear.