Using Cache And Compression For Performance

Sep 24, 2008
by:   Tim Stanley

Most browsers and servers have default values that provide fairly good performance with a minimum caching of static file content.  A little bit of coaxing can result in better performance with less traffic and less bandwidth.

There are two types of caching I discuss: static content  (*.JPG, *.JPEG, *.png, *.gif, *.ico) rarely updated and semi-static content (*.js, *.css) updated very infrequently.  The decision to cache page output (*.aspx, *.html, *.htm) long term is too dependent on application functionality decisions to discuss generically.

Summary

If you are short on time, the quick summary is summarized below.

Setting a future expiration date let's the browser be smarter about how it requests content only when absolutely necessary.  Compressing / Deflating the content for css, js, aspx and html content minimizes the amount of bandwidth used.  If you pay for bandwidth by the byte, compression is going to save money.

Windows Servers:

  1. If you have access to the IIS Admin console, set an expiration date.  This is cheap and quick.  You get caching via future expiration dates without compression and with no code changes.
  2. If you want to control individual files for compression and/or caching, but do not have IIS Admin access, you can configure a verb to process *.css.axd, *.jpg.axd, *.js.axd files and then change your references to the content.
  3. If you want to control individual files for compression and/or caching, install an httpHandler and configure the verb to process *.css, *.jpg, *.js or other files.  This also requires access to the IIS Admin console to set the  Application Extension Mapping, or if the project is installed via MSI, the mappings can be done at installation.

Apache Servers:

  1. Use Apache mod_expires to set the expiration date in the future.
  2. Modify .htaccess and set  Header unset ETag and FileETag None
  3. Use a compression module on like mod_gzip or something similar.

Expiration Dates:

  1. Images should be anywhere from 30 days to 5 years or more.
  2. CSS and JavaScript may need to be updated more frequently and should be anywhere from 1 day to 5 days.

One Page, Many Contents

When loading a web page, it typically isn't a single request.  It's just the tip of the iceberg.  The page loads, then the browser may need to load more content from that (like a CSS file that references images). The browser may also attempt to load the favicon.ico file even if the page doesn't ask to load it.

Here's an example of a single page that has 27 requests for various different content needed to support loading 1 page.

Cache 200

200's and 304's

Browsers have a conversation with the server to request content for a [url] that starts off something like this.

First time requests:

  1. Browser: Can I have the content for [URL]?
  2. Server: HTTP/1.0 200 OK -> here it is.
  3. Browser: I also need the content for [URL]
  4. Server: HTTP/1.0 200 OK -> here it is.
  5. Browser: I also need the content for [URL]
  6. Server: HTTP/1.0 200 OK -> here it is.

That happens over and over for every piece of content needed for the page to be displayed for the first time.  The second time the request is made, if the browser has enough information to make a decision, it gets a bit smarter.

Second + N requests:

  1. Browser: If the server sent an expiration time for the content, the browser may just use the last copy of the content (No request sent to the server)
  2. Browser: If the server didn't send an expiration time, request the content (depends on browser settings).
  3. Server: If it was updated or no eTag match: HTTP/1.0 200 OK -> here it is.
  4. Server: If it's the same: HTTP/1.1 304 Not Modified -> Use what you have, it's hasn't changed.

Cache 200 2nd

The difference between loading the same page shown above is 1.78 seconds for the first time and 0.108 seconds the second time. The time savings is due to the fact that all but one piece of content is cached in the browser for the second request.

Browser Settings

Although you can't force users to do it, browser settings can affect how they ask servers for the information.

A unique query parameter on a URL will cause IE 7 to believe it's a "different" URL and IE will be more inclined to request data even though it already has it.  For example traces I've seen with timestamp's as part of the query parameter will cause IE to request JPG files and receive 304 responses.  If the query parameter is not part of the URL, IE will not request the content again until a refresh is requested.

The following value controls how IE uses it's cache for HTTP content (not just pages).

IE History Settings

 

Firefox 3 has both the Private Data (cache) setting and the options that control how it utilizes the cache.

Firefox Private Data

Firefox Options

Entity Tags

Entity Tags (ETags) are used to provide additional detail for checking caching.  Yahoo Rules for high performance web sites and YSlow recommend setting the ETag values to improve performance.

By default, Apache and IIS use ETags. If you are running a web farm, remove ETags or ensure the ETags are syncronized across the farm (refer to the Microsoft support article to remove etags in IIS).

In Apache to remove ETags, modify the .htaccess or httpd.conf file:


Header unset ETag
FileETag None

If ETags are used, the request from the browser will contain something like the following:

If-Modified-Since: Tue, 08 Jul 2008 05:52:56 GMT
If-None-Match: "0ac6adebee0c81:893"

In ASP.Net code, this is set with a statement like:


Response.Cache.SetETagFromFileDependencies();

Refer to the Microsoft support article to remove etags in IIS or to set ETags, refer to  Enabling Client-Side Caching of Generated Content in ASP.NET for a more detailed explanation.

Save Requests, Use A Cache

If you have a large number of users, the 304 responses add additional HTTP requests and traffic.  These can be eliminated or reduced in IIS and ASP.Net in three ways: IIS Content Expiration, ASP.Net *.css httpHandlers, or with an ASP.Net *.axd httpHandler.

IIS Content Expiration

If you have access to IIS administration set the IIS expiration cache to an appropriate value.  Unlike Apache, I haven't figured out how to set different expiration dates for images versus CSS and JavaScript files.

IIS Content Expiration

 

ASP.Net httpHandler *.css.ashx *.css.axd

Sample content references:

<link rel="stylesheet" type="text/css" href="csshandler.axd?path=style.css" mce_href="csshandler.axd?path=style.css" />
<link rel="stylesheet" type="text/css" href="style.css.axd" mce_href="style.css.axd" />

Adding an httpHandler in the web.config.


<httpHandlers>
    <add path="*.css.axd" verb="*" type="Utils.SFHandler" validate="false"/>
</httpHandlers>

The article at Caching in ASP.NET explains in more detail how to use this approach.  This approach does not require IIS administration permissions, but it does leave the CSS or other source references tied to unusual file extensions for css, js, and images.

Caution: You must take measures to ensure that only specific types of files desired are output.  If this is not done properly in code, using the path=abc.css approach, could allow someone to load a normally secure file (like the web.config) using this technique.

Once you have the handler in place, it's simple:


Response.Cache.SetExpires ( DateTime.Now.AddMinutes ( 86400 ) )
Response.Cache.SetCacheability ( HttpCacheability.Public )

 

Refer to the code example for the BlogEngine.Net CSS handler for more detail.

ASP.Net httpHandler *.css

Sample content reference:

<link rel="stylesheet" type="text/css" href="style.css" mce_href="style.css" />

Adding an httpHandler in the web.config.


<httpHandlers>
    <add path="*.css" verb="*" type="Utils.SFHandler" validate="false"/>
</httpHandlers>

The approach is exactly the same as in Caching in ASP.NET, but this time some more configuration work is required.  However, with this approach an additional application IIS Extension Mapping is required.  If you have access to the IIS administration console, this is easy for the site.  If you are in a commercial hosting environment, it's much more difficult.

In IIS, this is set on the Home or Virtual Directory, Configuration, Mappings, Add

IIS App Config

IIS App Config Add

Executable = C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll
Extension = .css
Limit To = GET,HEAD,POST,DEBUG

Apache mod_expires

In Apache, an future expiration date can be set with with Apache mod_expires.


ExpiresActive On
ExpiresByType image/gif A2592000
ExpiresByType image/png A2592000
ExpiresByType image/jpg A2592000
ExpiresByType image/jpeg A2592000

Save Bandwidth, Use Compression

Compression can reduce the size of a css, js or the output of php or aspx files by 60-80%. Yahoo performance research notes:

40-60% of Yahoo!’s users have an empty cache experience and ~20% of all page views are done with an empty cache.

 

Even though these files can be cached, by enabling compression for these files, you reduce both the bandwidth and the load time required for the file.  If you pay for bandwidth by the byte, this will save money.

All this bandwidth savings comes at a cost of CPU.  If the results for static content are cached, the result is minimal.  If you have a large number of users accessing a site and they are accessing it over the WAN, not LAN, then compression is in my opinion a better option over the CPU cost.  If your on a LAN or have a low number of users, compression isn't going to benefit compared to the CPU cost.

Mads Kristensen talks about stripping white spaces in the article Reduce the weight of stylesheets by 35% at runtime but his article on HTTP compression in ASP.NET 2.0 covers how to make compression work for the ASPX output and this can be applied to css and js files as well.

Warning: Using compression can break some javascripts, if used on the Webresources.axd, and can break some third party components, so use this with caution.

For information on Webresource compression, refer to Miron Abramson's New & Shiny WebResource.axd compression Module.

BlogEngine.Net

BlogEngine.Net is a wonderful blogging platform (it's used for this site) and it has a lot of example code for these techniques listed here.  For more information, refer to the BlogEngine.core modules image.axd, css.axd, and js.axd.

Sample code from BlogEngine.Net CSS Handler


private static void SetHeaders(int hash, HttpContext context)
{ 
   context.Response.ContentType = "text/css";
   context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;
 
   context.Response.Cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
   context.Response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
   context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
 
   string etag = "\"" + hash.ToString() + "\"";
   string incomingEtag = context.Request.Headers["If-None-Match"];
 
   context.Response.Cache.SetETag(etag);
 
   if (String.Compare(incomingEtag, etag) == 0)
   {
       context.Response.Clear();
       context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
       context.Response.SuppressContent = true;
   }
}

Recommendations

  1. Minimize the number of HTTP requests by minimizing the unique content that must be loaded on a page.
  2. Set an expiration time to enable the cache to eliminate HTTP requests that would involve 304 responses. 30+ days for images, 1+ days for CSS and JavaScript files.
  3. Use Compression for page output, CSS and JS.
  4. Do not use compression for images (the algorithms for JPG, PNG already compress data for the image).

Search Terms

  • ASP.net enable content expiration
  • ASP.net expires header
  • ASP.net 304 set expires time images
  • ASP.net httphandler verb css
  • HttpCacheability.Public

References

I've listed the articles that I found appropriate in my research on this topic.

Related Items