KARPACH

WEB DEVELOPER BLOG

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.

YSlow

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.

CSS Sprite

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

http://www.dotnetkicks.com/kick/?url=http%3a%2f%2fwww.karpach.com%2fyslow-and-asp-net-100-points-a-grade.htm

Posted on February 28, 2009 by

Comments

Posted on 3/2/2009 06:30:13 AM by theclown

Hey thanks, I can now save some bandwidth by browsing your blog without ads!

Posted on 3/2/2009 02:16:17 PM by Mark

Once you combine all your css and javascript using msbuild, do you also replace the references in your html? How do you do this?

Posted on 3/3/2009 02:09:56 AM by recursieve

same question as theclown, how do you update your view to use the new combined name for your css/js?

Yes, I do replace references based on web.config value.

<appSettings>
  <clear/>;
  <add key="CDNPath" value="http://karpach.appspot.com/cdn/" />
  <add key="IsProduction" value="true" />
</appSettings>

You can have two PlaceHolder-s in your head section. One for your local development server and one for production server.

Just hide and show correct section base on web.config value.

Posted on 3/3/2009 07:47:45 AM by Thomas Hansen

We have only A scores except for the CDN thing, and it’s all right out of the box with our Ajax Library ;) - http://ra-ajax.org

Thomas,

As far as I can see you do not gzip page itself :-)

Posted on 3/12/2009 11:31:57 PM by Paul

You are just fooling yourself into thinking you have an A but removing everything that would bring the grade down.

By not measuring the performance with external resources you’ve completely invalidated the tests. They may take you down to an F due to load times, size, not gizipping, not setting a far future expires header, etc. If this is the case you’d want to reevaluate their use.

First of all even if I turn everything on a grade is C(75), which is not bad at all.

Technically you are right. However on another hand I can’t optimize Google Ads, Google Analytics or Dotnetkiks (this is what I am disabling). Since I can’t optimize it, why should I count it? Anyway this staff is minor and doesn’t affect user experience of how fast my blog shows up. Who cares if ads shows up later, anyway they are rendered in iframe.

Posted on 5/5/2010 10:07:58 AM by Sean

Ah, I have been stuck at 99 in ySlow for one of my sites and it’s driving me nuts. I have no idea where to make up the 1 point!!!!
Which category does not have A grade?

Posted on 9/3/2010 07:18:57 AM by Fausto

Hi,

I’m trying to use your HttpModule for add expire headers to statics files, but seems that request for images,(png, gif,jpg) are not handled by HttpModule,(only aspx,adx, etc.).

Can you help me?

If you are using IIS6 then you need to add extension mapping in IIS. See http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/5c5ae5e0-f4f9-44b0-a743-f4c3a5ff68ec.mspx?mfr=true. You can do this in IIS7 too, but there is a simpler solution.

Posted on 2/15/2012 02:36:16 AM by ramo2712

Hi,

The 3 point “3. Add an Expires header”.

Where I have to call the “Init” method and how I get the “HttpApplication context” parameter that needs to be passed to the method.

Thanks,

Omar

In Visual Studio Solution Explorer do right click and select add new item. On left side select web and then on right side click on ASP.NET Module. Visual Studio would create a starting point for your HttpModule. Init method would be there. Since it is HttpModule it would be called by ASP.NET framework, just don’t forget to register it in web.config.