Optimize your WebApp like a PRO – ASP.NET MVC Boilerplate

March 16, 2014/POSTED BY Marjan Nikolovski/1 Comment
We are living in a time when technology is developing with fast pace. Today most of the users can enjoy the privilege of having an internet connection, using modern web browsers and using modern web applications. As a result of the technology, browsers can now host and serve complex applications. Rule of the thumb is that no matter how speed the internet connection is, if not properly develop, the web application that your team is building might end up with a not so great user experience at the end.

In order to excel our customer expectations we might need to go one step further into optimizing what we’ve developed so far or even better – to develop our next web application with optimization practices in mind.

If you wonder what you need to do there is a great development check list published by Yahoo! development team. Most of the rules are general development/configuration rules. I will go into technical “How to” for implementing them in ASP.NET MVC/IIS.

First thing first, we will start with the simple rules.

Add style sheets in the HEAD

This is a simple rule that combine several sub rules like:
  • Forget about using inline styles and internal style sheets
  • Use external style sheets and put them under the HEAD tag


Add your scripts at the bottom of the <body>

image

User defer tag in <script src=”PATH_TO_SCRIPT” defer/> to prevent blocking from the html render

Add favicon

All modern browsers will first try to resolve your favicon. Add favicon to speedup page resolve and load. You will notice that faulted requests that try to access the favicon will disappear from your web app log file. Don’t forget to add favicon route exception in you ASP.NET MVC web app.
routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });


Minimize request for the static data by using CSS sprites

Put all of your icons and assets that you are using for your design into one file. Create CSS file to access the resources. You will minimize n*request per resource time that the browser would call for the separate assets.

There is a great online tool for creating CSS from a sprite. Check out Sprite cow – http://www.spritecow.com.

Once you’ve got the images that will be used in your web app, use https://tinypng.com/ to compress the sprites. The service will trim all of the metadata in the sprites and minimize the file size.

Configure IIS and your webapp for GZIP and Cacheing

To configure IIS please use this guide from Microsoft http://technet.microsoft.com/en-us/library/cc730629%28v=ws.10%29.aspx. Once successfully configured under system.webServer configuration section add:
<system.webServer>
    <staticContent>
      <remove fileExtension=".js" />
      <remove fileExtension=".css" />
      <mimeMap fileExtension=".js" mimeType="text/javascript" />
      <mimeMap fileExtension=".css" mimeType="text/css" />
      <clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="500.00:00:00" />
    </staticContent>
    <urlCompression doStaticCompression="true" doDynamicCompression="true" />
</system.webServer>


Minimize your CSS and JavaScript Files

In order to minimize your CSS and JavaScript files you will need to install SquishIt and SquishIt MVC extensions. Personally I prefer SquishIt over ASP.NET Bundle. The following syntax can be used in your views to minimize the CSS files:

@(Bundle.Css()
.Add("~/Content/base.css")
.Add("~/Content/CSS/Plugins/BootstrapDatepicker/daterangepicker-bs3.css")
.MvcRender("~/Content/CSS/min/combined_#.css"))

… And for Javascript:
@(Bundle.JavaScript()
.Add(@"~/Scripts/lib/jquery-2.0.2.min.js")
.Add(@"~/Scripts/lib/jquery.slimscroll.min.js")
.Add(@"~/Scripts/lib/jquery-ui-1.10.3.custom.min.js")
.Add(@"~/Scripts/lib/typeahead.min.js")
.Add(@"~/Scripts/lib/daterangepicker.js")
.Add(@"~/Scripts/lib/moment.min.js")
.MvcRender("~/Scripts/min/combined_#.js"))


Trickiest of all – Cookie less domain

For webapps that use Forms authentication this optimization is mandatory. You may ask why? When the user is authenticated, the web application will create an authentication cookie. The cookie is encrypted for security and the standard size is somewhere around 2KB. Each request that your browser will create while trying to retrieve the static content like: images, CSS files, JavaScript files will contain the cookie. So we have back and forth 2KB per requests. This sucks! We are left with two options:
  • Pay for a a content delivery network
  • Trick the browser
The first option is easy. Lets try the second one. Create an CNAME(alias) named cdn.yourwebsite.com in your zone file. Point the alias to your web application domain.

image

Now we will use a modified version of an MVC cache breaker:
namespace System.Web.Mvc
{
    public static class CdnExtensions
    {
        private static readonly Regex ScriptRegex = new Regex("<script.+?src=[\"'](.+?)[\"'].*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
        private static readonly Regex CssRegex = new Regex("<link.+?href=[\"'](.+?)[\"'].*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);

        public static string CdnContent(this UrlHelper url, string link, bool isDynamicResource = false)
        {
            link = link.ToLower();

            // last write date ticks to hex
            string cacheBreaker = string.Empty;

            if (!isDynamicResource)
            {
                cacheBreaker = Convert.ToString(File.GetLastWriteTimeUtc(url.RequestContext.HttpContext.Request.MapPath(link)).Ticks, 16);
            }

            link = link.TrimStart(new[] {'~', '/'});
            link = string.Format("{0}/{1}", “BASE_CDN_URL”, link);

            // returns the file URL in static domain
            return isDynamicResource ? link : string.Format("{0}?v={1}", link, cacheBreaker);
        }

        public static MvcHtmlString CdnContent(this UrlHelper url, MvcHtmlString mvcLink, bool isDynamicResource = false)
        {
            var context = mvcLink.ToHtmlString().ToLower();
            // match the scripts
            var linksMatches = ScriptRegex.Matches(context);
            var sb = new StringBuilder();
            foreach (Match linkMatch in linksMatches)
            {
                var link = linkMatch.Groups[1].Value;
                link = CdnContent(url, link, isDynamicResource);
                var script = string.Format("<script type='text/javascript' src='{0}'></script>", link);
                sb.AppendLine(script);
            }
            // match the styles
            linksMatches = CssRegex.Matches(context);
            foreach (Match linkMatch in linksMatches)
            {
                var link = linkMatch.Groups[1].Value;
                link = CdnContent(url, link, isDynamicResource);
                var script = string.Format("<link href='{0}' rel='stylesheet' type='text/css' />", link);
                sb.AppendLine(script);
            }

            return new MvcHtmlString(sb.ToString());
        }
    }
}


HTML:
@(Url.CdnContent(Bundle.Css()
.Add("~/Content/base.css")
.Add("~/Content/CSS/Plugins/BootstrapDatepicker/daterangepicker-bs3.css")
.MvcRender("~/Content/CSS/min/combined_#.css")))

… And for Javascript:
@(Url.CdnContent(Bundle.JavaScript()
.Add(@"~/Scripts/lib/jquery-2.0.2.min.js")
.Add(@"~/Scripts/lib/jquery.slimscroll.min.js")
.Add(@"~/Scripts/lib/jquery-ui-1.10.3.custom.min.js")
.Add(@"~/Scripts/lib/typeahead.min.js")
.Add(@"~/Scripts/lib/daterangepicker.js")
.Add(@"~/Scripts/lib/moment.min.js")
.MvcRender("~/Scripts/min/combined_#.js")))

What is your experience with Web Apps optimization? Happy hacking!

About Marjan Nikolovski

Co-owner, Team lead Full stack .NET and Javascript engineer, speaker and active open source contributor. Excited about Distributed programming, Software architecture, Cloud based solutions.

1 Comment

  • boban984@gmail.com'
    April 05, 2014

    Put the code on github …

    Great blog post

Leave a comment

Your email address will not be published. Please mark all required fields.

You must be logged in to post a comment.