YSlow and ASP.NET: 100 points "A" grade is possible
Just in case, if somebody doesn’t know what YSlow is? YSlow is a Firefox add-on integrated with Firebug. It analyzes web pages and tells you why they are slow. You can run it on your project and see your grade. You can do some easy improvements and maybe even get a D grade. However, getting a grade higher than a D grade becomes a real challenge. Below I will describe how I got an A grade (100 points) on my web blog application.
First of all, you don’t want to measure performance with any external stuff which is beyond your control such as Google Ads, Google Analytics, Dotnetkicks. So, you need to make a query string switch, which is going to disable/hide all this stuff. In my case it is PageType=NoExternal, so the full URL would be http://www.karpach.com/default.aspx?PageType=NoExternal. Also, you need to add CDN setting for YSlow. My personal CDN is http://karpach.appspot.com/cdn/. Now if you measure the performance of my default.aspx?PageType=NoExternal, you would see that it is A (100). Now let’s see how achieved this.
1. Make fewer HTTP requests.
First of all, you need to combine stylesheets and javascript files in one file. You should do this only for your production build using MS Build tasks.
<ItemGroup>
<TextFiles Include="*.css" Exclude="global.css"/>
</ItemGroup>
<Exec Command="echo y| type %(TextFiles.Identity) >> global.css"/>
This way you can combine your custom CSS and javascript files.
Some developers would ask: What about WebResource.axd? New AJAX Control Toolkit has ToolkitScriptManager, which can combine most of your WebResource.axd files. It also gzip them (this is important for later sections).
Next, you need to create CSS Sprites. You can read CSS Sprites: Image Slicing’s Kiss of Death article to get a better idea of what is CSS Sprites. The article doesn’t discuss how to create sprites for repeated backgrounds. The idea is to group such backgrounds. For example, all vertical repeating backgrounds in vbackground.gif and all horizontal repeating backgrounds in hbackground.gif. Then you can use background-position: -OffsetPixels, 0px for vertical repeated backgrounds and background-position: 0px, -OffsetPixels for horizontal repeated backgrounds. Here is an example of CSS Sprite from my blog application.
2. Use CDN (Content Delivery Networks)
Content Delivery Networks are expensive. I googled for something cheap and found the following article 10 Easy Steps to use Google App Engine as your own CDN. Of course, you need to do a few adjustments to make it a perfect CDN.
First of all default cache expiration is 600 sec. However, YSlow wants it to be at least 7 days. So, in your app.yaml after api_version add the following line:
**default\_expiration: "7d"**
Again you can use msbuild to modify a stylesheet file to use CDN. Here is how I did this:
<Import Project=".\References\MSBuild.Community.Tasks.targets" />
<Target Name="Release">
<FileUpdate Files="$(OutputPath)styles\basic.css" Regex="\.\.\/images/([^\)]*)" ReplacementText="http://karpach.appspot.com/cdn/images/$1" />
</Target>
You need to put on CDN all your images, styles and JavaScript files. Here is a cache problem that comes up. What if some visitor accessed your application just before you released a new version, which has a modified stylesheet and some modified images? This somebody is going to have a cached version of a stylesheet file and images for 7 days, so he is not going to see changes until his browser cache expires. This is not good. If a question like this comes up then it is time to see what yahoo did on its web site. For images, you can see that they have a date stamp and a version in the image name. For example, trough_2.0_062308.gif. It makes sense. If you modify one of your CSS sprites just rename it manually with a new date stamp and version.
However, you probably don’t want to use the same approach for your stylesheet file. First of all you want to track all versions of your stylesheet in a version control system (in my case subversion). For images, it doesn’t matter, since you don’t change them too often. My idea is to have just one physical CSS stylesheet in my case basic.css. However, on a page, I would use it like this: basic_14102262.css, where 14102262 is a current version of the application. Then you can use URL rewrite techniques, so basic_14102262.css redirects to basic.css. A user who has a cached version of basic_14102261.css would be forced to load a new version basic_14102262.css, since the file name changed.
Google Aps engine makes URL rewrite easy. Modify your app.yaml to have the following:
- url: /cdn/styles/basic\_\d\*\.css
static\_files: cdn/styles/basic.css
upload: cdn/styles/basic\.css
3. Add an Expires header
Everything under CDN would already have it. For all other files I created HttpModule:
private readonly static string[] CACHED_FILE_TYPES = new string[] { ".jpg", ".gif", ".png",".css" };
public void Init(HttpApplication context)
{
context.AcquireRequestState += new EventHandler(context_AcquireRequestState);
}
void context_AcquireRequestState(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
if (context != null && context.Response != null)
{
string fileExtension = Path.GetExtension(context.Request.PhysicalPath).ToLower();
if (context.Response.Cache != null && Array.BinarySearch<string>(CACHED_FILE_TYPES, fileExtension) >= 0)
{
HttpCachePolicy cache = context.Response.Cache;
TimeSpan duration = TimeSpan.FromDays(365);
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetValidUntilExpires(true);
cache.SetNoServerCaching();
cache.SetMaxAge(duration);
}
}
}
Ideally, you won’t need this module, since all qualified files should be under CDN.
4. Gzip components
You can read Building a GZip JavaScript Resource Compression Module for ASP.NET article on how to create gzip HTTP module. Just keep in mind that IE6 doesn’t have 100% gzip support.
5. Put CSS at the top.
This is an easy one. It’s just common sense. Keep this in mind when you are developing server controls. Use Header.Controls collection to add your stylesheet include.
6. Put JS at the bottom.
Sometimes it’s really difficult to follow this rule. For example, I always want a jquery script to include at the top, so I don’t need to worry if a library script is loaded or not. Anyway for your custom scripts use an inline script to check if a library is loaded and then use it.
For example:
Library.js
function DoSomething()
{
}
And then inline script:
<script>
if (typeof (DoSomething) == 'undefined')
{
alert('Library is not loaded yet');
}
</script>
7. Avoid CSS expressions.
CSS expressions work only in IE, so if you build a cross-browser application you should not use them anyway. The only good use for them is min-width/min-height hack for IE6.
8. Make JS and CSS external.
This is also common sense. Just read what Yahoo suggests http://developer.yahoo.com/performance/rules.html#external
9. Reduce DNS lookups.
Try to copy external resources (images, javascript) to your websites.
For example, my blog has a link to a W3 validation service. Originally icon for it was located on a W3 Schools web site, so I copied it to my project. Now, this icon is local, so I have 1 less DNS lookup.
10. Minify JS.
Do minification during production build using MS Build. I using a YUI compressor for this purpose. Here is an example of the MS Build script.
<Target Name="Compress">
<Message Text="Create temp files ..." />
<Copy SourceFiles=".\$(ProjectName)\Javascript\ColorPicker.js" DestinationFiles=".\$(ProjectName)\Javascript\ColorPicker.js.full"/>
<Copy SourceFiles=".\$(ProjectName)\Styles\ColorPicker.css" DestinationFiles=".\$(ProjectName)\Styles\ColorPicker.css.full"/>
<Exec Command="java -jar yuicompressor-2.4.2.jar --type js .\$(ProjectName)\Javascript\ColorPicker.js.full >.\$(ProjectName)\Javascript\ColorPicker.js"/>
<Exec Command="java -jar yuicompressor-2.4.2.jar --type css .\$(ProjectName)\Styles\ColorPicker.css.full >.\$(ProjectName)\Styles\ColorPicker.css"/>
</Target>
11. Avoid Redirects
You should not have a problem with this. Just read what yahoo recommends.
12. Remove Duplicate Scripts
Be careful when you are developing custom server and user controls. Make sure that for multiple controls of the same kind include script just one time.
13. Configure ETags
The cache module provided above and CDN should take care of this issue. Just keep in mind that ASP.NET has a special method for ETags:
Response.Cache.SetETag
P.S. The article was mentioned on Channel 9 this week: Martin Woodward, MVP Summit, Web Perf, Show Off, and a VSTS Pep Talk
P.S.S YSlow and ASP.NET: 100 points “A” grade year 2012 update article