Introduction

According to Best Practices for Speeding Up Your Web Site, 80% of the end-user response time is spent on the front-end. Most of this time is tied up in downloading all the components in the page: images, stylesheets, scripts, Flash, etc.

So it's bother-worthy to put attention on the front-end, though, there are also a lot of things need to handle in the back-end. This post will talk about several performance pitfalls in the browser and the steps to address them in ASP.NET MVC.

Well, rather than talking the abstract concept of performance, we need some tools to help measure it. There are a bunch of tools are already in the place, such as Developer Tools in Chrome, already integrated in the browser and extremely handy, Fiddler, a .net based application to capture all http traffics go in and out. However, in this post, I would like to use firebug, a sophisticated tool advanced along with the development of the web. And use Page Speed to make the benchmark (actually, there is a mark system in it). So the optimisation is fairly visible and rational.

Background

Long enough for the introduction, at any rate, let's get started. First off, I am involved a ASP.NET MVC project which uses a lot of CSS files and JavaScript files. It's not uncommon in today's web project as we can utilise many sorts of libraries off the shelf, like JQuery, everybody loves it, and YUI, an awesome library has a lot of UI widgets. Here's the project my fellow programmers are implementing. Let's have a glance at the css and js references.

    <!-- Individual YUI CSS files -->
    <link rel="stylesheet" href="<%: Url.Content("~/yui/reset-fonts-grids/reset-fonts-grids.css") %>" />
    <link rel="stylesheet" href="<%: Url.Content("~/yui/base/base-min.css") %>" />
    <link rel="stylesheet" href="<%: Url.Content("~/yui/assets/skins/sam/skin.css") %>" />
    <link rel="stylesheet" href="<%: Url.Content("~/Content/Site.css") %>" />
    <!-- Individual YUI JS files -->
    <script src="<%: Url.Content("~/yui/utilities/utilities.js") %>"></script>
    <script src="<%: Url.Content("~/yui/datasource/datasource-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/autocomplete/autocomplete-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/container/container-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/selector/selector-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/event-delegate/event-delegate-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/event-mouseenter/event-mouseenter-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/menu/menu-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/button/button-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/calendar/calendar-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/carousel/carousel-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/json/json-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/swf/swf-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/charts/charts-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/slider/slider-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/colorpicker/colorpicker-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/cookie/cookie-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/paginator/paginator-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/datatable/datatable-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/datemath/datemath-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/editor/editor-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/element-delegate/element-delegate-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/event-simulate/event-simulate-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/history/history-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/resize/resize-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/imagecropper/imagecropper-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/imageloader/imageloader-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/layout/layout-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/progressbar/progressbar-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/swfstore/swfstore-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/storage/storage-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/stylesheet/stylesheet-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/tabview/tabview-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/treeview/treeview-min.js") %>"></script>
    <script src="<%: Url.Content("~/yui/uploader/uploader-min.js") %>"></script>
    <script src="<%: Url.Content("~/Scripts/CommonUI.js") %>"></script>

I know it a little bit looooong. Tons of css and js files are included. But I hope it didn't make you dizzy because we need continue anyway. Now, it's time for Page Speed's debut. Let's see what it can show us.

image

Well, we can spot a couple of things. The first one come to our attention is the poor mark of performance (73 over 100). Then several performance gaps we need to fill,

Basically, these advices can been categorised into 3 groups, caching, compression and parallel connection. At the face of it, they looks like they are all independent. While into the core of these three respects, they have highly interdependence and interrelationship. Technically, caching resources can reduce the http request dramatically as cached items are read from local disk. And having compressed files (html, css, js) transferred over the network would cost much less time than raw ones. Consequently, the efficiency of the connection and transferring is improved accordingly due to the optimisation of caching and compression.

In a nutshell, any adjustment made in one aspect of the three will affect other twos.

Optimisation

Enough for the appetiser. Now, time to make our hands dirty. I am about to start from the caching. Actually, there are at least two ways can cope with it. For one, we can use the traditional HttpHandler which existed in ASP.NET Architecture for years and it is functional in ASP.NET WebForm. For another, we are also able to create a Filter. Since we are talking about the optimisation in ASP.NET MVC. So, only implementation of Filter will be introduced in this post. (I will introduce a HttpHandler implementation in another post, keep tuned ;p)

Connection

We have a list of reference (including 4 CSS files and 37 JavaScript files) to download each time we view the page of site, as it’s defined in the master page, this is really inefficient because the browser can only make rather limited HTTP requests to the same host simultaneously. So, Page Speed advice us to Parallelize downloads across hostnames. However, I would like not to follow Page Speed’s advice as it’s more related to the deployment. Instead, I will try to reduce the number of requests, this is more practical from a developer’s view.

First off, let’s create a Controller called SiteController and write one action called Js. Here’s the code snippet.

    public class SiteController : Controller
    {
        private static StringBuilder JsContent { get; set; }

        public ActionResult Js(string id)
        {
            if (JsContent == null)
            {
                JsContent = new StringBuilder();

                foreach (var jsFile in StaticFiles.JsFiles)
                {
                    var jsFilePath = Path.Combine(Request.PhysicalApplicationPath, jsFile);

                    JsContent.AppendLine(System.IO.File.ReadAllText(jsFilePath));
                }
            }

            return Content(JsContent.ToString(), "text/javascript");
        }
    }

N.B. StaticFiles.JsFiles in the code are pre-defined location of CSS and JS files.

Then, in the master page replace JavaScript references with following statement.

    <!-- Individual YUI JS files -->
    <script type="text/javascript" src="<%: Url.Action("Js", "Site", new { id = "site.js" }) %>"></script>

Now, we are all set. The idea of the code is to combine JavaScript files into one virtual file. As a result, the events of HTTP request will be near 97% off, from 40 to 1, this is really breathtaking. Let’s quickly use Page Speed to evaluate our changes.

image

Great! Evidently, we have achieved the goal of Parallelize downloads across hostnames and we got 4 more points.

Compression

We have successfully reduced the HTTP request, though, the file file wasn’t reduced at all. Let’s check out the “big guy” in the following request table.

image

The file size of site.js is 976KB. It takes up about 90% of the size of whole page (1.1MB). So we need to launch an “anti-obesity” campaign to help it lose its weight.

Firstly, create a Filter called CompressionFilterAttribute in ASP.NET MVC project and the body of it should look like the following.

    public class CompressionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var request = filterContext.HttpContext.Request;

            string acceptEncoding = request.Headers["Accept-Encoding"];

            if (string.IsNullOrEmpty(acceptEncoding))
            {
                return;
            }

            acceptEncoding = acceptEncoding.ToUpperInvariant();

            var response = filterContext.HttpContext.Response;

            if (acceptEncoding.Contains("DEFLATE"))
            {
                response.AppendHeader("Content-encoding", "deflate");
                response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
            }
            else if (acceptEncoding.Contains("GZIP"))
            {
                response.AppendHeader("Content-encoding", "gzip");
                response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
            }
        }
    }

Secondly, apply Compression Filter to the Js action of SiteController like this.

        [CompressionFilter]
        public ActionResult Js(string id){
            // ...
        }

Cool, let’s re-check the request table and see what happed.

image

Still remember the file size before optimised? Yes, 976KB. And it’s only 286KB after optimised. In other words, the compression rate is 29%. And time to call out Page Speed.

image

Apparently, Enable compression is achieved and the grade is 80 out of 100. We just moved more steps forward.

Caching

Eventually, the caching. Let’s dissect the cache in detail. To begin with, let’s have a look at the cache information of site.js.

image

As we can see that file isn’t cached at all. So, we need to create an Filter to apply cache just like CompressionFilter we just created. The following code is the content of CacheFilter.

    public class CacheFilterAttribute : ActionFilterAttribute
    {
        public int Duration { get; set; }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            if (Duration <= 0) return;

            var cache = filterContext.HttpContext.Response.Cache;
            var cacheDuration = TimeSpan.FromDays(Duration);

            cache.SetCacheability(HttpCacheability.Public);
            cache.SetExpires(DateTime.Now.Add(cacheDuration));
            cache.SetMaxAge(cacheDuration);
            cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
            cache.SetLastModified(DateTime.MinValue);
            cache.SetETag("v1.0");
        }
    }

N.B. Duration stands for the days of the content to be cached.

And, apply the Filter to Js action like this.

        [CacheFilter(Duration = 30)]
        [CompressionFilter]
        public ActionResult Js(string id)
        {
            // ...
        }

I wanted site.js to be cached for 30 days. So, let’s review the cache information of site.js.

image

See? Expire Date is updated to Feb 16th 2011. We successfully applied the cache capability. And as usual, Page Speed time.

image

From the result, we did have one point plus, but because we only optimised for the site.js, we still did not iron out the Caching issue. Consequently, we need to do more to cover Css files and images. Actually, it’s not a trivial work. I will tackle them in another post. Keep tuned.

Wrap Up

To wrap up, through advices provided by Page Speed, we have optimised the web site rationally by creating a SiteController to combine several JavaSript files into one file, implementing two Filters to apply compression and caching. As a result, the HTTP Requests and file size was reduced by 97% and 71% separately, which is rather sensational.

 

References

Combine, compress, and update your CSS file in ASP.NET MVC

ASP.NET MVC ACTION FILTER - CACHING AND COMPRESSION

You're Reading The World's Most Dangerous Programming Blog

 posted on 2011-01-17 23:32  助平君  阅读(1351)  评论(4编辑  收藏  举报