Minify HTML with .NET MVC ActionFilter
To make our site a tight as possible, I thought we’d explore the idea of removing the white space in our generated HTML.
I’ve had this idea for a long time, but after reading @hugoware post about removing white space, I decided to implement it in my own projects. As with any good code, let’s stand on the shoulder of giants and make it fit our needs.
Normally, if we did a “View Source” on our web page we would see something like this:
Now a lot of good HTML developers like to leave closing html comments and notes in their code. This helps them track end elements in nested code. For example:
<div id="wrap"> <div id="content"> content goes here, with more elements of <> </div> <!--end content--> </div> <!--end wrap -->
The issue we may face on larger pages is there can be a lot of notes and end line comments which can add to the overall size of your page, which the user doesn’t care about but the developer needs for easy editing later.
So how do we achieve this?
We need to minify the generated HTML, thus removing the comments and whitespace as the page is run. Very much the same process we did when minifying our CSS and JavaScript in an earlier post.
As we are working on an .NET MVC project we will look at building an ActionFilter.
The tools
- Microsoft Visual Studio 2010 or Trail editions (Download via Web platform installer)
- A little time to look good.
Creating the ActionFilter
We will start as always with a standard vanilla MVC (Model View Controller) .NET application install, good old “File > New Project > ASP.NET MVC Web Application”
Next we will create a folder to hold our ActionFilter, mm what should I call it?
Right click on this folder and add a new class called ‘RemoveWhiteSpace.cs’ or whatever you thing is best for you. Your class should look something like this.
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace RemoveWhiteSpace.ActionFilters { public class WhiteSpaceFilter { } }
Now to add our Inheritance on the Stream and change our ‘Using’ statements to only the ones we need. Thus
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace RemoveWhiteSpace.ActionFilters { public class WhiteSpaceFilter : Stream { } }
The Stream requires we implement inherited abstract members, so let’s add the basics to our class and alter the constructor to allow for stream input and filter params
<pre>using System; using System.IO; using System.Text; using System.Web.Mvc; using System.Text.RegularExpressions; namespace RemoveWhiteSpace.ActionFilters { public class WhiteSpaceFilter : Stream { private Stream _shrink; private Func<string, string> _filter; public WhiteSpaceFilter(Stream shrink, Func<string, string> filter) { _shrink = shrink; _filter = filter; } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return true; } } public override bool CanWrite { get { return true; } } public override void Flush() { _shrink.Flush(); } public override long Length { get { return 0; } } public override long Position { get; set; } public override int Read(byte[] buffer, int offset, int count) { return _shrink.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) { return _shrink.Seek(offset, origin); } public override void SetLength(long value) { _shrink.SetLength(value); } public override void Close() { _shrink.Close(); } public override void Write(byte[] buffer, int offset, int count) { // capture the data and convert to string byte[] data = new byte[count]; Buffer.BlockCopy(buffer, offset, data, 0, count); string s = Encoding.Default.GetString(buffer); // filter the string s = _filter(s); // write the data to stream byte[] outdata = Encoding.Default.GetBytes(s); _shrink.Write(outdata, 0, outdata.GetLength(0)); } } }
Next we need to add the ActionFilter Attribute. This allows us to decorate our controller later with our minify filter.
<pre>public class WhitespaceFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.HttpContext.Request; var response = filterContext.HttpContext.Response; response.Filter = new WhiteSpaceFilter(response.Filter, s => { s = Regex.Replace(s, @"\s+", " "); s = Regex.Replace(s, @"\s*\n\s*", "\n"); s = Regex.Replace(s, @"\s*\>\s*\<\s*", "><"); s = Regex.Replace(s, @"<!--(.*?)-->", ""); //Remove comments // single-line doctype must be preserved var firstEndBracketPosition = s.IndexOf(">"); if (firstEndBracketPosition >= 0) { s = s.Remove(firstEndBracketPosition, 1); s = s.Insert(firstEndBracketPosition, ">"); } return s; }); } }</pre>
We are almost done, we just need to put it all together and decorate our HomeController.cs with the new ActionFilter. First we need to add another ‘Using’ to HomeController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using RemoveWhiteSpace.ActionFilters; //Minify HTML Filter
Now to minify our HomeController, you can decorate either on the whole controller or just on an ‘ActionResult’
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using RemoveWhiteSpace.ActionFilters; //Minify HTML Filter namespace RemoveWhiteSpace.Controllers { [HandleError] [WhitespaceFilter] public class HomeController : Controller { public ActionResult Index() { ViewData["Message"] = "Welcome to ASP.NET MVC!"; return View(); } public ActionResult About() { return View(); } } }
With the Filter running we can now see our HTML source code looks a lot different.
This is just a basic page, but comparing the standard HTML page against the minified version we can see we have reduced the download file size even before we add Gzip/Deflate
Before:
After:
Finally
In the last few posts we have covered some of the basics of speeding up your web-site. Putting all these together can really make your pages fly. Now it doesn’t end there, it might be worth you looking at sockets and other alternative options, base64 image in-line to reduce the number http calls. Speed is an ever moving target.
I hope what I shown you over the last few posts helps.
Full project code is available for download (See right hand navigation)
If you found this helpful and would like to buy me a beer to say thanks, please
Nice post, Arran. I like how you used the ActionFilterAttribute to capture the markup, very nice.
One thing that that has bitten me before was removing white space and comments in Javascript. For example.
After ‘minifying’ all of the content ends up on a single line… which looks like this…
Oops… That is a problem that ended up breaking my entire page… 😐
Thanks for the comments (Standing on the shoulders of Giants). Minifying JavaScript and CSS, I leave it to the Build Task. So it processed before up loading to the site. Less work for the server to be doing. 🙂
You dont have to do it at runtime
http://omari-o.blogspot.com/2009/09/aspnet-white-space-cleaning-with-no.html
With Razor view engine it’s even easier
OmariO seem have a alternative solution that does not seem to require the filter at runtime, but instead is compiled at buildtime. I’ll have to have a play with his code and report back.
This method is not good because it incur runtime performance costs. I tried benchmarking the performance and I found out that although the size of the page is 50% to 60% compressed but the time rendered is 70% to 100% slower at firstload compared to uncompressed page. This method has disadvantage as mentioned here http://j.mp/I4iZCU
I’ll give this a try, The faster the better. I’ve not really noticed the first load times due to pre-caching work on first run. So hopefully this will make it even quicker.
If you have the full details of the benchmark tests stated (we’ll like a few graphs), I’ll post a link to them here.
Thanks for sharing and the great work, Keep Rocking.
I think not only at first load but all the time using your ActionFilter compression approach. You can benchmark your method by creating two MVC applications of exactly have the same contents. Use your compression method in the first application but leave the second one as is. Use Firebug on Firefox to see all the request for both applications. You will see that in your method even though the pages are compressed but the time to rendered all pages are longer compare to uncompressed mvc application. Therefore for web application where speed or performance is at stake, your compression method is NOT acceptable.
Thanks for your comment, always good to have feed back and a discussion. I’ve downloaded the example code from the blog link. It’s doesn’t work for me. There is ZERO Whitespace removal. Un-altered solution from the zip file has Zero effect, I also followed the readme file to double check the settings, Zero effect (MVC 2). If anyone has a working example of this code and would like to share, that would be great.
But I’ll keep trying and report back when I have time.
If anyone has alternative working CODE examples to help improve the performance, please share.
Thanks for reading, now get back to work.
Why do you remove then re-insert the first end bracket? with Html5 doctypes I don’t see any difference commenting the following code
// single-line doctype must be preserved
var firstEndBracketPosition = s.IndexOf(“>”);
if (firstEndBracketPosition >= 0)
{
s = s.Remove(firstEndBracketPosition, 1);
s = s.Insert(firstEndBracketPosition, “>”);
}
Very nice method of minifying the response. It is, however; removing IE conditional comments.
To solve the issue I replaced the following line:
s = Regex.Replace(s, @”“, “”);
With
s = Regex.Replace(s, @”<!–(?!\s*(?:\[if [^\]]+]|))(?:(?!–>).)*–>”, “”);
I liked it so much, but it minifies codes inside the pre tags. To avoid that use this solution:
http://stackoverflow.com/questions/16875114/regex-minify-pre-tag-content
Hi Friends,
Can we write unit test cases for the WhitespaceFilterAttribute class. If yes then how ?
because I tried a lot to creating using test cases but didn’t work with HttpResponse.Filter and control doesn’t go into regex filter check.
can anyone help me.
Thanks.
I’d like to echo vegabitax’s question. I’ve removed that part from my code because it seems unnecessary. I’ll paste the original question:
Why do you remove then re-insert the first end bracket? with Html5 doctypes I don’t see any difference commenting the following code
// single-line doctype must be preserved
var firstEndBracketPosition = s.IndexOf(“>”);
if (firstEndBracketPosition >= 0)
{
s = s.Remove(firstEndBracketPosition, 1);
s = s.Insert(firstEndBracketPosition, “>”);
}
Just to let you know there’s a small bug in the code:
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
string s = Encoding.Default.GetString(buffer);
the last line should use the data variable and not the buffer parameter so
string s = Encoding.Default.GetString(data);
otherwise when hooking up multiple response filters the HTML will become corrupt because the offset en count aren’t correct in subsequent calls.
So, i have problem with UTF-8 data and same as #1 above comment