Squashing the asp.net MVC response - part 1
The goal of this post : reduce the total size of a simple asp.net MVC page response.
Our measuring tools : Firefox running Firebug and the YSlow plugin
Source Code : Download here
Lets use a really simple and common scenario as the example. The steps to create this really simple example are:
- Create a new MVC project in visual studio.
- Dump some useful script files into the scripts folder and delete the others we dont need.
- Create a new .css file in the Content folder and cut half the css out of the site.css and paste it into this new file. (this is just to create more than 1 css file)
- Open the site.master file and add references to the new files in the head.
Why are we doing this? Well this is what you would most likely do when adding functionality to your new MVC site. You would add a few references to some scripts you will use in the views, and you would more than likely add some references to more css files. (I personally hate combining css into one large file, and instead prefer to break it up into logical files)
The resulting site.master would look something like this:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
<link href="../../Content/Layout.css" rel="stylesheet" type="text/css" />
<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
<script src="../../Scripts/jquery.utils.js" type="text/javascript"></script>
<script src="../../Scripts/jqDnR.js" type="text/javascript"></script>
</head>
<body>
<!- extracted -->
</body>
</html>
Let's compile and run the MVC site. Open it in FireFox. Turn on firebug and see what u get. I got this Net breakdown:
Note at this stage the number of requests (6) and the total download size (135KB) and it took 4.11 seconds. Also notice that when you refresh the page, ALL the page components are downloaded AGAIN. Nothing is cached. This is not good. Now the YSlow breakdown :
See our YSLOW score is 73!! WOW thats bad. You can expand each section to see exactly what you can do to improve the performance. Here it is expanded :
Now we are going to work through the page to improve our YSlow score and our overall download size. In this post I am going to go straight into the solution. I will use future posts to explain the code behind the changes and the reasons why I did what I did. I am also going to be using both code I have found on the web and some code I have written myself (with some ideas taken from the web). Again, all my code will be explained in future posts. This post is just an intro while giving a solution. (cause I hate blog series where you have to wait untill the last one to get the solution, especially seeing that I write about 1 post every 6 months - i promise i will try harder!)
I have put all the source together into my own Utils components. I have spoken about these a while ago and they have changed dramatically since then. There are 4 files you need to reference from the web project :
- Unity - Dependency injection framework used by the utils project
- Utils - common utils and helper classes incl. encryption wrappers, collections, string utils, a number of extension methods, etc etc
- Utils.Web - common web utils and helpers not specific to either webforms or MVC incl querystring helpers and a URL helper to name a few
- Utils.Web.MVC - common MVC specific classes and utils.
Please note that the source for the utils in this post has changed alot since the original blog posts, and someday I will write some more blog posts on the more interesting and useful classes inside.
Firstly, lets start with the easy one : gzip and cache the page response. I found some really good code at Kazi Manzur Rashid's blog. In his post he gives the solution to both caching and compressing the page response, by utilising 2 action flters. Just change the home controller in our project to the following :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Utils.Web.MVC.Filters;
namespace MvcApplication2.Controllers
{
[HandleError]
[CacheFilter]
[CompressFilter]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
return View();
}
}
}
Our YSLOW score is now 75 and our total download size is 1K less. So we are getting there, but slowly...
Next, we need to do something about the CSS. We are now going to combine, gzip and cache the stylesheets. Follow these steps:
- Add a namespace reference in the web.config :
<add namespace="Utils.Web.MVC"/>
- Add a httphandler to the web.config :
<add verb="*" path="css.axd" type="Utils.Web.HttpHandlers.CSSHandler, Utils.Web" validate="false"/>
- Change the site.master. Cut out the old references to the stylesheets and replace with this in the head:
<% Html.CSS().Add("~/Content/Site.css"); %>
<% Html.CSS().Add("~/Content/Layout.css"); %>
<%= Html.CSS().HTML %>
Cool. With a few changes to the HTML, we get the same response but with a few improvements:
- Our YSLOW score has jumped up to 83!
- We now have less requests (5) and our total file size is down to 130KB.
- Add the httphandler to the web.config :
<add verb="*" path="js.axd" type="Utils.Web.HttpHandlers.JSHandler, Utils.Web" validate="false"/>
- Change the site.master. Cut out the old script references from the head and at the bottom of the file just before the closing of the body tag, add the following code:
<% Html.Scripts().Add("~/Scripts/jquery-1.3.2.js"); %>
<% Html.Scripts().Add("~/Scripts/jquery.utils.js"); %>
<% Html.Scripts().Add("~/Scripts/jqDnR.js"); %>
<%= Html.Scripts().HTML %>
Now what has changed? Well some major changes have occurred in the HTML output which has resulted in the following:
- Our YSLOW score is a whopping 98!!! WOW!! Thats more like it. The only thing we are not getting an A score for is the Content Delivery Network section.
- But more importantly, our number of requests has dropped to 3 and the total size is only 27KB!
- Our total transfer time is now only 2.08 seconds!
The HTML output is now the following:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>
Home Page
</title><link rel="stylesheet" type="text/css" href="/css.axd?f=%2fContent%2fSite.css%3fm%3d1%2c%2fContent%2fLayout.css%3fm%3d1&d=365" media="screen" /></head>
<body>
<div class="page">
<!-- extracted for example -->
</div>
<script type="text/javascript" src="/js.axd?f=%2fScripts%2fjquery-1.3.2.js%3fm%3d1%2c%2fScripts%2fjquery.utils.js%3fm%3d1%2c%2fScripts%2fjqDnR.js%3fm%3d1&d=365"></script>
</body>
</html>
The resulting firebug Net screenshot is:
And here is the YSlow screenshot:
Conclusion:
With very few (and simple) changes there has been a massive improvement:
- 6 requests down to 3
- 135K size down to 27K
- YSlow score from 73 up to 98
- Download time from 4.11s down to 2.08s
I also feel the changes have not been too hard to make and they still leave the view code in a very readable state (this is important in MVC). In my follow-on posts I want to go through the CSS and JS handlers that combine,gzip and cache the files. I will go through how your views and even your partial views can 'tell' the page what scripts or css to include and combine, and eliminate duplicates if necessary. No longer does the master page have to know about all the scripts that will be used within the whole site up front.
Download the source. (Check out the Test.aspx view and PartialTest.ascx partial view to see a more realistic example)