Tips & Tricks: Improving the performance of your website
As a part of my job, I have spent quite a bit of time thinking about performance over the last year – more specifically, the performance of web applications. Websites generally struggle with many of the same performance problems, though which depends on the scale of the system. The problems fall into two general categories and over the course of two articles, I will present a number of tips and tricks for improving the performance of a website or system. This article focuses on the first category which is the client-facing performance issues such as file sizes, number of http requests, browser caching, etc. The other article will focus on the performance issues happening while the server is processing the request.
The client-facing performance issues tend to be most relevant to smaller websites and systems, since server-side performance issues related to data and databases tend to grow far worse for complex systems, if not kept in check.
Most of the tips in these articles are specific to .NET, ASP.NET or ASP.NET MVC but the general principles are applicable to any web platform.
YSlow and Page Speed
As mentioned above, web systems are prone to many of the same problems and several tools have appeared to deal with this. The tools YSlow form Yahoo and Page Speed from Google are two such utilities. They are both plugins for Firefox and what they do is perform a static analysis on one or more pages of your site, giving them a grading based on a large set of best practices and methods. The perfect score is a 100, but this can be quite hard to reach, as some of the advice given by the tools is not really relevant for all types of websites. I suggest you use both tools as they don't overlap 100% and they do provide different suggestions and guidance.
Tools such as YSlow and Page Speed should probably be the first stop when you begin to optimize the client-facing part of a website as they can give you a more or less complete overview of all the aspects that can cause your pages to load slowly.
Resource minification and merging
One of the things that can really slow your pages down is not the page itself but all the extra resources it has to pull down from the server, such as images, stylesheets and script files. Each of these will start an http request and the browser is limited in how many open connections it will allow to a single domain at a time. For high-traffic sites, the number of connections can also become an issue for the web server, causing even longer load times.
The best way to avoid this problem is to merge all your CSS styles into a single file and similarly for your JavaScript files. This will give you two http requests for each page no matter how many styles and scripts you actually have, leaving you to structure your files as you want. Also, while we're at it, we might as well minify the files as well. Minification is just the process of reducing the size of the files without changing their function, by removing unneeded white space and similar optimizations such as changing variable names to shorter versions.
Many tools exist for doing this, and I've taken a couple of them and integrated them into an easy to use resource loader. It leverages the web.config for specifying which resources to include and how to process them; to use an example, this is how it's configured for my blog:
- <configSections>
- <section name="resourceLoaderSettings" type="Web.Performance.ResourceLoaderConfiguration" />
- </configSections>
- <resourceLoaderSettings minify="false" cache="false" cacheDuration="525948">
- <folders>
- <add path="/Content/themes/base/minified" extensions="css" />
- <add path="/Content/css" extensions="css" />
- <add path="/Scripts/ExternalIncluded" extensions="js" ordering="jquery-1.6.3.min.js,jquery-ui-1.8.16.custom.min.js" />
- <add path="/Scripts/Local" extensions="js" />
- </folders>
- </resourceLoaderSettings>
Notice that I have disabled minification and caching in the configuration. I use the web.release.config transformation file to turn both of these on when deploying, giving you the best of both worlds for debugging and release. To manage dependencies between different scripts or styles, you can optionally specify a comma-separated list of which resources to add first. The loader is clever enough to realize that files with min as a part of the name are already minimized. A thing that is not shown in this example is the support for searching in subfolders which can be enabled for each individual folder by setting the attribute recursiveSearch on the folder to true.
Having configured the loader, the next step is to update the Razor layout file to include links to each resource. Using a helper method I will describe in a moment, this is done as follows:
- <link href="/Files/SiteStyle/@Resources.GetCssHash()/sitestyle.css" rel="stylesheet" type="text/css" />
- <script src="/Files/SiteScript/@Resources.GetJavaScriptHash()/sitescript.js" type="text/javascript"></script>
The Resource helper computes the hash value of the different resource files, resulting in a url that changes every time the file is modified. We exploit this to cache the files for the maximum allowed duration (one year), since updates to the files would result in a new uncached url. The resource loader has helper methods for determining the hash value of the files, so the helper class simply looks like this:
- public static class Resources
- {
- public static string GetJavaScriptHash()
- {
- return WebResourceLoader.GetJavaScriptHash();
- }
- {
- return WebResourceLoader.GetCssHash();
- }
- }
Now, we just need to provide the actual file content and to do this, we must create a route matching the urls above to two actions (I leave this as an exercise to the reader). Getting the content is simply a matter of calling a static method on the loader, so the most interesting part of this code is setting up the proper caching and compression settings. I won't go into details with the implementation of the actions other than to say that the implementation of the Compress attribute can be found here and that the Forever profile is a standard ASP.NET cache profile set to one year.
- [Compress]
- [OutputCache(CacheProfile = "Forever")]
- public JavaScriptResult SiteScript(string version)
- {
- var resourceLoader = new WebResourceLoader();
- Response.Cache.SetETag(version);
- return JavaScript(resourceLoader.GetJavaScript());
- }
- [Compress]
- [OutputCache(CacheProfile = "Forever")]
- public ContentResult SiteStyle(string version)
- {
- var resourceLoader = new WebResourceLoader();
- Response.Cache.SetETag(version);
- return Content(resourceLoader.GetCssStyle(), "text/css");
- }
The only thing I haven't shown yet is the implementation of the loader but there is too much code to include in this article so you will just have to download it and see for yourself. The code includes a couple of minifiers that I found on the web but you can use any implementation you'd like by implementing the IMinifier interface.
Image data url helper
Following in the same vein as the previous section, we would also like to reduce the number of http requests for images on the page. Modern browsers support an alternative way to define images than the standard url scheme, namely data urls. By base-64 encoding the image data, you can insert it directly into the src attribute of the image, thereby avoiding the extra http request. There are some caveats with this technique, though, such as the modern browser thing I just mentioned. To be sure, you should test it in the browsers that you target, but it should work in IE8+ as well as newer versions of all the other major browsers. Each browser has its own limit on exactly how big the images are allowed to be, but I believe the standard requires support for up to 4KB. In practice, you can have somewhat larger images, with IE8 being the lowest I know of, allowing a maximum of 32KB. Note that images tend to grow about 30-40% when converted to base-64, so don't just use it blindly. My rule of thumb is to use this technique for icon-type images and not photos and the like.
You don't really want to go out and calculate the base-64 code for each of your images, so I have created a helper class that can do it for you. To make it as easy as possible to use, I hardcode the path to the image folder into the implementation but this ties the code into the specific project. You can set it using a static property when starting the application if you want to use it across projects. You can use it like this in a Razor view:
- <img src="@Img.Src("logo.png")" />
The implementation relies on another performance tool I haven't introduced yet, so you will have to try and guess what some of the code does :) In the second article I will introduce the CacheProvider class and the mystery will be revealed.
- public static class Img
- {
- private static readonly object _cacheLockObject_Src = new object();
- private const string ImagePathFormat = "/Content/Images/Site/{0}";
- public static MvcHtmlString Src(string filename)
- {
- //IE before version 8 do not support Data URIs
- var supportsDataUris =
- HttpContext.Current.Request.Browser.Browser != "IE" ||
- HttpContext.Current.Request.Browser.MajorVersion > 7;
- //IE8 Only allows Data URIs of less than 32KiB
- var maxSize = HttpContext.Current.Request.Browser.Type == "IE8" ? (1 << 15) - 1 : int.MaxValue;
- var cacheKey = string.Format("Img-Src-{0}", filename);
- Func<MvcHtmlString> query = () =>
- {
- if (!supportsDataUris)
- return new MvcHtmlString(string.Format(ImagePathFormat, filename));
- var path = Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, string.Format(@"Content\Images\Site\{0}", filename));
- var file = new FileInfo(path);
- using (var s = file.OpenRead())
- {
- var data = s.GetBytes();
- var encoded = Convert.ToBase64String(data);
- var prefix = "data:image/gif;base64,";
- var result = prefix + encoded;
- if (result.Length > maxSize)
- result = string.Format(ImagePathFormat, filename);
- return new MvcHtmlString(result);
- }
- };
- return CacheProvider.CacheQuery(query, cacheKey, _cacheLockObject_Src, CacheProvider.Forever);
- }
- }
That's it. I hope you found it useful and perhaps got you thinking a bit more about how you can improve the performance of your site. One thing to note, though, the next version of the MVC framework will have its own implementation of the merging and minification functionality, so if you can wait that long, there is no reason to mess around with it yourself. I also think they have something up their sleeve with regards to base-64 encoding images, but I'm, not sure about that one. For a great walkthrough of how to optimize your website with the next version of MVC and IIS, see the talk by Mads Kristensen from the recent Microsoft Build conference.
Comments