Home > Code Examples > Minify HTML with .NET MVC ActionFilter

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:

MVC Vanilla app, view source.

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

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”

File new project Default MVC screen

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

@ArranM

Categories: Code Examples Tags: , , ,
  1. August 11, 2010 at 3:23 pm

    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.

    <script type="text/javascript">
        //just an innocent comment
        alert('hello');
    </script>

    After ‘minifying’ all of the content ends up on a single line… which looks like this…

    <script type="text/javascript">//just an innocent comment alert('hello'); </script>

    Oops… That is a problem that ended up breaking my entire page… 😐

  2. ArranM
    August 11, 2010 at 3:33 pm

    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. 🙂

  3. OmariO
    October 25, 2010 at 1:19 pm

    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

  4. ArranM
    October 26, 2010 at 12:04 am

    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.

  5. April 23, 2012 at 10:18 am

    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

    • ArranM
      April 24, 2012 at 7:45 am

      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.

  6. Jobzky
    April 30, 2012 at 6:05 am

    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.

    • @ArranM
      April 30, 2012 at 7:27 am

      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.

  7. December 13, 2012 at 10:55 am

    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, “>”);
    }

  8. Jbzea
    March 20, 2013 at 7:30 pm

    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 [^\]]+]|))(?:(?!–>).)*–>”, “”);

  9. June 3, 2013 at 2:32 pm

    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

  10. Manoj
    June 18, 2013 at 4:10 pm

    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.

  11. September 25, 2013 at 11:22 pm

    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, “>”);
    }

  12. Rooc
    January 28, 2014 at 10:05 am

    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.

  13. March 11, 2014 at 9:24 am

    So, i have problem with UTF-8 data and same as #1 above comment

  1. January 2, 2011 at 3:27 pm

Leave a reply to Manoj Cancel reply